From d58e554bb389c61d03c8043deef908901dc8afd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Suszy=C5=84ski=20Krzysztof?= Date: Thu, 6 Jul 2017 11:15:38 +0200 Subject: [PATCH] Adding on_unsupported_os method impl+tests --- .rubocop.yml | 1 - Gemfile | 8 +- Gemfile.lock | 40 ++- lib/rspec-puppet-facts-unsupported.rb | 7 +- .../on_unsupported_os.rb | 289 ++++++++++++++++++ metadata.json | 11 + rspec-puppet-facts-unsupported.gemspec | 2 +- .../on_unsupported_os_spec.rb | 94 ++++++ .../version_spec.rb} | 0 spec/spec_helper.rb | 1 + 10 files changed, 444 insertions(+), 9 deletions(-) create mode 100644 lib/rspec-puppet-facts-unsupported/on_unsupported_os.rb create mode 100644 metadata.json create mode 100644 spec/rspec-puppet-facts-unsupported/on_unsupported_os_spec.rb rename spec/{rspec-puppet-facts-unsupported_spec.rb => rspec-puppet-facts-unsupported/version_spec.rb} (100%) diff --git a/.rubocop.yml b/.rubocop.yml index e26fee3..88b2c44 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -14,7 +14,6 @@ Style/FileName: - 'Rakefile' - '*.gemspec' - lib/rspec-puppet-facts-unsupported.rb - - spec/rspec-puppet-facts-unsupported_spec.rb Metrics/BlockLength: Exclude: - 'spec/**/*_spec.rb' diff --git a/Gemfile b/Gemfile index 2026f69..5eb8f53 100644 --- a/Gemfile +++ b/Gemfile @@ -7,12 +7,14 @@ def req(req_s) end group :test do - gem 'rspec', '~> 3', require: false - gem 'rubocop', '~> 0.49', require: false if req('>= 2.0') =~ RVERSION - gem 'simplecov', '~> 0.14.1', require: false + gem 'rspec', '~> 3', require: false + gem 'rspec-collection_matchers', '~> 1.1.3', require: false + gem 'rubocop', '~> 0.49', require: false if req('>= 2.0') =~ RVERSION + gem 'simplecov', '~> 0.14.1', require: false end group :development do + gem 'bundler', '~> 1.15.1', require: false gem 'pry-byebug', '~> 3.4', '>= 3.4.2', require: false if req('>= 2.0') =~ RVERSION gem 'rake', '~> 10', require: false end diff --git a/Gemfile.lock b/Gemfile.lock index e3731d1..f49b893 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,6 +2,7 @@ PATH remote: . specs: rspec-puppet-facts-unsupported (0.1.0) + rspec-puppet-facts (~> 1.8.0) GEM remote: https://rubygems.org/ @@ -11,7 +12,27 @@ GEM coderay (1.1.1) diff-lcs (1.3) docile (1.1.5) + facter (2.4.6) + facterdb (0.3.11) + facter + jgrep + fast_gettext (1.1.0) + gettext (3.2.3) + locale (>= 2.0.5) + text (>= 1.3.0) + gettext-setup (0.25) + fast_gettext (~> 1.1.0) + gettext (>= 3.0.2) + locale + hiera (3.4.0) + jgrep (1.4.1) + json json (1.8.6) + locale (2.1.2) + mcollective-client (2.11.0) + json + stomp + systemu method_source (0.8.2) parallel (1.11.2) parser (2.4.0.0) @@ -24,6 +45,11 @@ GEM pry-byebug (3.4.2) byebug (~> 9.0) pry (~> 0.10) + puppet (5.0.0) + facter (> 2.0, < 4) + gettext-setup (>= 0.10, < 1) + hiera (>= 3.2.1, < 4) + locale (~> 2.1) rainbow (2.2.2) rake rake (10.5.0) @@ -31,6 +57,8 @@ GEM rspec-core (~> 3.6.0) rspec-expectations (~> 3.6.0) rspec-mocks (~> 3.6.0) + rspec-collection_matchers (1.1.3) + rspec-expectations (>= 2.99.0.beta1) rspec-core (3.6.0) rspec-support (~> 3.6.0) rspec-expectations (3.6.0) @@ -39,6 +67,12 @@ GEM rspec-mocks (3.6.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.6.0) + rspec-puppet-facts (1.8.0) + facter + facterdb (>= 0.3.0) + json + mcollective-client + puppet rspec-support (3.6.0) rubocop (0.49.1) parallel (~> 1.10) @@ -54,16 +88,20 @@ GEM simplecov-html (~> 0.10.0) simplecov-html (0.10.1) slop (3.6.0) + stomp (1.4.4) + systemu (2.6.5) + text (1.3.1) unicode-display_width (1.3.0) PLATFORMS ruby DEPENDENCIES - bundler (~> 1.15) + bundler (~> 1.15.1) pry-byebug (~> 3.4, >= 3.4.2) rake (~> 10) rspec (~> 3) + rspec-collection_matchers (~> 1.1.3) rspec-puppet-facts-unsupported! rubocop (~> 0.49) simplecov (~> 0.14.1) diff --git a/lib/rspec-puppet-facts-unsupported.rb b/lib/rspec-puppet-facts-unsupported.rb index d63c323..b3ca49b 100644 --- a/lib/rspec-puppet-facts-unsupported.rb +++ b/lib/rspec-puppet-facts-unsupported.rb @@ -1,6 +1,7 @@ -require 'rspec-puppet-facts-unsupported/version' - # A main module of rspec-puppet-facts-unsupported module RspecPuppetFactsUnsupported - # your code goes here + # a placeholder module end + +require 'rspec-puppet-facts-unsupported/version' +require 'rspec-puppet-facts-unsupported/on_unsupported_os' diff --git a/lib/rspec-puppet-facts-unsupported/on_unsupported_os.rb b/lib/rspec-puppet-facts-unsupported/on_unsupported_os.rb new file mode 100644 index 0000000..c0a054f --- /dev/null +++ b/lib/rspec-puppet-facts-unsupported/on_unsupported_os.rb @@ -0,0 +1,289 @@ +require 'rspec-puppet-facts' + +# A main module of rspec-puppet-facts-unsupported +module RspecPuppetFactsUnsupported + # Fetches an unsupported list of operating system's facts. List doesn't contains operating system + # described in Puppet's metadata.json file. + # @return Hash[String => Hash] A hash of key being os description and value being example + # machine facts + # @param Hash[Symbol => Object] opts A configuration hash with options + # @option opts [String,Array] :hardwaremodels The OS architecture names, i.e. 'x86_64', /IBM/ + # or 'i86pc' + # @option opts [Array] :supported_os If this options is provided the data + # will be used instead of the "operatingsystem_support" section if the metadata file + # even if the file is missing. + # @option opts [Array] :filters An array of extra filters to be passed to FacterDB to + # narrow the search + # @option opts [Array] :order An order in which records should be returned. You can pass values + # like :random - to randomly shuffle records (exact shuffle seed will be printed on stderr to be able + # to reproduce invalid behaivior), `:original` - to return original list, an integer seed - to reproduce + # failiures. By default it is set to `:random` + # @option opts [Array] :limit A limit of records to be returned. By default it is set to 2. + def on_unsupported_os(opts = {}) + process_opts(opts) + filters = calculate_filters(opts) + facts = FacterDB.get_facts(filters) + op = UnsupportedFilteringOperation.new(facts, opts) + op.facts + end + + def self.verbose=(verbose) + @@verbose = (verbose == true) # rubocop:disable Style/ClassVars + end + + def self.verbose? + @@verbose + end + + def verbose? + RspecPuppetFactsUnsupported.verbose? + end + + protected + + def factname(fact, opts = {}) + opts[:era] ||= :legancy + fact_sym = fact.to_s.to_sym + if opts[:era] == :legancy + fact_sym + elsif opts[:era] == :current + CURRENT_FACT_NAMES[fact_sym] + else + raise "invalid era: #{opts[:era].inspect}" + end + end + + private + + CURRENT_FACT_NAMES = { + hardwaremodel: :'os.hardware', + operatingsystem: :'os.name', + operatingsystemrelease: :'os.release.full', + operatingsystemmajrelease: :'os.release.major' + }.freeze + + @@verbose = true # rubocop:disable Style/ClassVars + + # Private class to perform randomization + class Randomizer + attr_reader :order, :seed + + def initialize(opts, envkey = :RSPEC_PUPPET_FACTS_UNSUPPORTED_ORDER) + opts[:order] ||= :random + randomize_seed + envvar = ENV[envkey.to_s] + ordervalue = envvar.nil? ? opts[:order] : envvar.to_sym + ilike = Integerlike.new(ordervalue) + self.seed = ilike.to_i if ilike.integer? + @order = ordervalue + end + + def get + @randomizer + end + + def should_randomize? + @order == :random || Integerlike.new(@order).integer? + end + + private + + SHORT_MAX = 2**16 + + def randomize_seed + @seed = Random.new.rand(0..SHORT_MAX) + end + + def seed=(seed) + @seed = seed + @randomizer = Random.new(@seed) + end + end + + def process_opts(opts) + opts[:randomizer] = Randomizer.new(opts) + ensure_default_hardwaremodels opts + ensure_default_filters opts + opts[:limit] ||= 2 + opts[:supported_os] ||= RspecPuppetFacts.meta_supported_os + process_order opts + end + + def ensure_default_hardwaremodels(opts) + opts[:hardwaremodels] ||= ['x86_64'] + opts[:hardwaremodels] = [opts[:hardwaremodels]] unless opts[:hardwaremodels].is_a? Array + end + + def ensure_default_filters(opts) + opts[:filters] ||= [] + opts[:filters] = [opts[:filters]] unless opts[:filters].is_a? Array + end + + def process_order(opts) + rnd = opts[:randomizer] + message = "Shuffling unsupported OS's facts with seed: #{rnd.seed}\nSet environment variable " \ + "to reproduce this order, for ex. on Linux \`export RSPEC_PUPPET_FACTS_UNSUPPORTED_ORDER=#{rnd.seed}\`" + $stderr.puts message if verbose? && rnd.should_randomize? + end + + def calculate_filters(opts) + filters = [] + opts[:hardwaremodels].each do |hardwaremodel| + filters << { + facterversion: "/^#{Regexp.quote(system_facter_version)}/", + hardwaremodel: hardwaremodel + } + end + filters = filters.product(opts[:filters]).collect { |x, y| x.merge(y) } unless opts[:filters].empty? + postprocess_filters(filters) + end + + def system_facter_version + Facter.version.split('.')[0..-2].join('.') + end + + def find_facter_version_matching_regexp(regexp) + (1..4).each do |major| + (0..7).each do |minor| + candidate = "#{major}.#{minor}.42" + stripped_regexp = regexp.gsub(%r{^/+|/+$}, '') + return candidate if Regexp.new(stripped_regexp).match(candidate) + end + end + nil + end + + def postprocess_filters(filters) + filters.map do |filter| + facterversion = find_facter_version_matching_regexp(filter[:facterversion]) + if facterversion.split('.').first >= '3' + current = factname(:hardwaremodel, era: :current) + legancy = factname(:hardwaremodel) + filter[current] = filter[legancy] + filter.delete(legancy) + end + filter + end + end + + # Integerlike private class + class Integerlike + def initialize(obj) + @obj = obj + end + + def integer? + to_s == to_i.to_s + end + + def to_i + to_s.to_i + end + + def to_s + @obj.to_s + end + end + + # Private class + class Facts + include RspecPuppetFactsUnsupported + def initialize(facts) + @facts = facts + end + attr_reader :facts + + def [](key) + normalized_key = factkey(key) + facts_by_path(facts, normalized_key) + end + + private + + def factkey(fact) + era = facter_current? ? :current : :legancy + factname(fact, era: era) + end + + def facts_by_path(facts, path) + stringified = Hash[facts.map { |k, v| [k.to_s, v] }] + path.to_s.split('.').inject(stringified) { |hash, key| hash[key] } + end + + def facter_current? + facterversion >= '3.0.0' + end + + def facterversion + facts[:facterversion] + end + end + + # Private opertion wrapper class + class UnsupportedFilteringOperation + include RspecPuppetFactsUnsupported + def initialize(facts_list, opts) + @opts = opts + @facts_list = facts_list + end + + def facts + postprocess_facts(reject_supported_os) + end + + private + + def reject_supported_os + metadata_supported = @opts[:supported_os] + @facts_list.reject do |candidate_raw| + candidate = Facts.new(candidate_raw) + req = metadata_supported.select do |single_req| + single_req['operatingsystem'] == candidate[:operatingsystem] + end + should_be_rejected?(req, candidate) + end + end + + def postprocess_facts(facts_list) + os_facts_hash = {} + facts_list.map do |facts| + description = describe_os(Facts.new(facts)) + facts.merge! RspecPuppetFacts.common_facts + os_facts_hash[description] = RspecPuppetFacts.with_custom_facts(description, facts) + end + shuffle_and_limit(os_facts_hash) + end + + def shuffle_and_limit(os_facts_hash) + randomizer = @opts[:randomizer] + as_array = os_facts_hash.to_a + as_array = as_array.shuffle(random: randomizer.get) if randomizer.should_randomize? + as_array = as_array[0..@opts[:limit]] + Hash[*as_array.flatten] + end + + def describe_os(facts) + "#{facts[:operatingsystem].downcase}-" \ + "#{facts[:operatingsystemmajrelease]}-" \ + "#{facts[:hardwaremodel]}" + end + + def should_be_rejected?(req, candidate) + if req.empty? + false + else + req = req.first + operatingsystemrelease_matches?(req['operatingsystemrelease'], candidate) + end + end + + def operatingsystemrelease_matches?(operatingsystemrelease, candidate) + if operatingsystemrelease.nil? + true + else + candidate_release = candidate[:operatingsystemrelease] + operatingsystemrelease.select { |elem| candidate_release.start_with?(elem) }.any? + end + end + end +end diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..5a6d7e4 --- /dev/null +++ b/metadata.json @@ -0,0 +1,11 @@ +{ + "name": "this-is-not-a-puppet-module", + "version": "999999999.0.0", + "author": "coi", + "summary": "Not a module, used just to verify RspecPuppetFactsUnsupported.on_unsupported_os method", + "license": "Apache-2.0", + "operatingsystem_support": [ + { "operatingsystem": "Ubuntu", "operatingsystemrelease": [ "14.04", "16.04" ] }, + { "operatingsystem": "CentOS", "operatingsystemrelease": [ "6", "7" ] } + ] +} diff --git a/rspec-puppet-facts-unsupported.gemspec b/rspec-puppet-facts-unsupported.gemspec index e8b77e1..2751789 100644 --- a/rspec-puppet-facts-unsupported.gemspec +++ b/rspec-puppet-facts-unsupported.gemspec @@ -24,5 +24,5 @@ Gem::Specification.new do |spec| spec.require_paths = ['lib'] spec.required_ruby_version = '>= 1.9.0' - spec.add_development_dependency 'bundler', '~> 1.15' + spec.add_dependency 'rspec-puppet-facts', '~> 1.8.0' end diff --git a/spec/rspec-puppet-facts-unsupported/on_unsupported_os_spec.rb b/spec/rspec-puppet-facts-unsupported/on_unsupported_os_spec.rb new file mode 100644 index 0000000..079d66e --- /dev/null +++ b/spec/rspec-puppet-facts-unsupported/on_unsupported_os_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +RSpec::Matchers.define :contain_os do |expected_os| + match do |all_oss| + !all_oss.select do |os_group| + actual_os, = os_group + actual_os == expected_os + end.empty? + end + description do + "contain operating system described as #{expected.inspect}" + end +end + +RSpec.shared_examples "it doesn't contain supported OS's described in metadata.json" do + it { is_expected.not_to contain_os('centos-6-x86_64') } + it { is_expected.not_to contain_os('centos-7-x86_64') } + it { is_expected.not_to contain_os('ubuntu-14.04-x86_64') } + it { is_expected.not_to contain_os('ubuntu-16.04-x86_64') } +end + +RSpec.describe RspecPuppetFactsUnsupported do + describe '#on_unsupported_os' do + before(:each) { RspecPuppetFactsUnsupported.verbose = false } + let(:target) { Class.new { extend RspecPuppetFactsUnsupported } } + subject { target.on_unsupported_os(opts) } + + context 'with no parameters given' do + let(:opts) { {} } + it { is_expected.to be_a Hash } + it { is_expected.to have_at_least(2).items } + it_behaves_like "it doesn't contain supported OS's described in metadata.json" + end + + context 'with :order opt' do + context 'set to 11111' do + let(:opts) { { order: 11_111 } } + it { is_expected.to be_a Hash } + it { is_expected.to have_at_least(2).items } + describe 'first returned operating system\'s facts' do + it { expect(Hash[*subject.first]).to contain_os('fedora-23-x86_64') } + end + context 'with verbose => true' do + before(:each) { RspecPuppetFactsUnsupported.verbose = true } + it { expect { subject }.to output(/^Shuffling unsupported OS's facts with seed: 11111/).to_stderr } + it { expect { subject }.to output(/export RSPEC_PUPPET_FACTS_UNSUPPORTED_ORDER=11111/).to_stderr } + end + it_behaves_like "it doesn't contain supported OS's described in metadata.json" + end + context 'set to :random' do + let(:opts) { { order: :random } } + it { is_expected.to be_a Hash } + it { is_expected.to have_at_least(2).items } + context 'with verbose => true' do + before(:each) { RspecPuppetFactsUnsupported.verbose = true } + it { expect { subject }.to output(/^Shuffling unsupported OS's facts with seed: \d+/).to_stderr } + it { expect { subject }.to output(/export RSPEC_PUPPET_FACTS_UNSUPPORTED_ORDER=\d+/).to_stderr } + end + it_behaves_like "it doesn't contain supported OS's described in metadata.json" + end + end + + context 'with :limit opt set to 100' do + let(:opts) { { limit: 100 } } + it { is_expected.to be_a Hash } + it { is_expected.to have_at_least(25).items } + it { is_expected.to contain_os('scientific-7-x86_64') } + it { is_expected.to contain_os('oraclelinux-6-x86_64') } + it { is_expected.to contain_os('debian-8-x86_64') } + it { is_expected.to contain_os('debian-7-x86_64') } + it_behaves_like "it doesn't contain supported OS's described in metadata.json" + end + + context 'with :hardwaremodels opt set to "/IBM/" and :filters opt to facterversion: "/^3\./"' do + let(:opts) do + { hardwaremodels: '/IBM/', filters: { facterversion: '/^3\./' } } + end + it { is_expected.to be_a Hash } + it { is_expected.to have_at_least(2).items } + it { is_expected.to contain_os('aix-7100-IBM,8231-E1D') } + it_behaves_like "it doesn't contain supported OS's described in metadata.json" + end + + context 'with :hardwaremodels opt set to "i86pc"' do + let(:opts) do + { hardwaremodels: 'i86pc' } + end + it { is_expected.to be_a Hash } + it { is_expected.to have_at_least(1).items } + it { is_expected.to contain_os('solaris-11-i86pc') } + it_behaves_like "it doesn't contain supported OS's described in metadata.json" + end + end +end diff --git a/spec/rspec-puppet-facts-unsupported_spec.rb b/spec/rspec-puppet-facts-unsupported/version_spec.rb similarity index 100% rename from spec/rspec-puppet-facts-unsupported_spec.rb rename to spec/rspec-puppet-facts-unsupported/version_spec.rb diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 69f59de..9928ca1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,6 +14,7 @@ def gem_present(name) require 'pry' if gem_present 'pry' require 'bundler/setup' +require 'rspec/collection_matchers' require 'rspec-puppet-facts-unsupported' RSpec.configure do |config|