From abd13301bae1b1274f3655c7ef1bb63b960f50f4 Mon Sep 17 00:00:00 2001 From: Wolfgang Teuber Date: Thu, 14 Sep 2023 23:59:25 +0200 Subject: [PATCH] Appease Rubocop and Mutant, Update README * update mutant config * update rubocop config * Set Rubocop TargetRubyVersion to required_ruby_version defined in gemspec (3.0) * Clean up gemspec and Gemfile * Update README * remove mutant from default rake task * Stop globally suppressing test example outputs * Add namespacing to specs --- .mutant.yml | 10 ++ .rubocop.yml | 8 +- Gemfile | 16 +++ Guardfile | 2 +- README.md | 107 ++++++++------ Rakefile | 12 +- lib/yaml_normalizer/ext/sort_by_key.rb | 2 +- lib/yaml_normalizer/helpers/normalize.rb | 8 +- lib/yaml_normalizer/helpers/param_parser.rb | 20 ++- lib/yaml_normalizer/rake_task.rb | 31 ++-- lib/yaml_normalizer/services/check.rb | 3 +- lib/yaml_normalizer/services/is_yaml.rb | 1 + lib/yaml_normalizer/services/normalize.rb | 3 +- spec/ci_helper.rb | 2 +- spec/data/valid2.yml | 4 +- spec/data/valid2_normalized.yml | 2 +- spec/data/valid_bom.yml | 8 ++ spec/nested_spec.rb | 48 ------- spec/rake_task_spec.rb | 114 --------------- spec/services/base_spec.rb | 40 ------ spec/services/check_spec.rb | 95 ------------ spec/services/is_yaml_spec.rb | 31 ---- spec/services/normalize_spec.rb | 123 ---------------- .../param_parser_shared_example.rb | 36 +++-- spec/sort_by_key_spec.rb | 45 ------ spec/spec_helper.rb | 14 -- .../ext}/namespaced_spec.rb | 9 +- spec/yaml_normalizer/ext/nested_spec.rb | 38 +++++ spec/yaml_normalizer/ext/sort_by_key_spec.rb | 46 ++++++ spec/yaml_normalizer/rake_task_spec.rb | 112 +++++++++++++++ spec/yaml_normalizer/services/base_spec.rb | 42 ++++++ spec/yaml_normalizer/services/check_spec.rb | 120 ++++++++++++++++ spec/yaml_normalizer/services/is_yaml_spec.rb | 45 ++++++ .../services/normalize_spec.rb | 136 ++++++++++++++++++ spec/yaml_normalizer_spec.rb | 2 +- yaml_normalizer.gemspec | 20 +-- 36 files changed, 714 insertions(+), 641 deletions(-) create mode 100644 .mutant.yml create mode 100644 spec/data/valid_bom.yml delete mode 100644 spec/nested_spec.rb delete mode 100644 spec/rake_task_spec.rb delete mode 100644 spec/services/base_spec.rb delete mode 100644 spec/services/check_spec.rb delete mode 100644 spec/services/is_yaml_spec.rb delete mode 100644 spec/services/normalize_spec.rb delete mode 100644 spec/sort_by_key_spec.rb rename spec/{ => yaml_normalizer/ext}/namespaced_spec.rb (63%) create mode 100644 spec/yaml_normalizer/ext/nested_spec.rb create mode 100644 spec/yaml_normalizer/ext/sort_by_key_spec.rb create mode 100644 spec/yaml_normalizer/rake_task_spec.rb create mode 100644 spec/yaml_normalizer/services/base_spec.rb create mode 100644 spec/yaml_normalizer/services/check_spec.rb create mode 100644 spec/yaml_normalizer/services/is_yaml_spec.rb create mode 100644 spec/yaml_normalizer/services/normalize_spec.rb diff --git a/.mutant.yml b/.mutant.yml new file mode 100644 index 0000000..d941733 --- /dev/null +++ b/.mutant.yml @@ -0,0 +1,10 @@ +--- +includes: + - lib +requires: + - yaml_normalizer +integration: + name: rspec +matcher: + subjects: + - YamlNormalizer* diff --git a/.rubocop.yml b/.rubocop.yml index 1c24680..5460555 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,9 +1,7 @@ require: - rubocop-performance + - rubocop-rspec AllCops: - TargetRubyVersion: 3.2 - -# Commonly used screens these days easily fit more than 80 characters. -Metrics/LineLength: - Max: 120 + TargetRubyVersion: 3.0 + NewCops: enable diff --git a/Gemfile b/Gemfile index d4c9fff..3afdc4d 100644 --- a/Gemfile +++ b/Gemfile @@ -5,3 +5,19 @@ source 'https://rubygems.org' ruby '>= 3.0' gemspec + +gem 'flog' +gem 'guard' +gem 'guard-rspec' +gem 'guard-rubocop' +gem 'inch' +gem 'mutant-license' +gem 'mutant-rspec' +gem 'pry-byebug' +gem 'pry-doc' +gem 'rspec' +gem 'rubocop' +gem 'rubocop-performance' +gem 'rubocop-rspec' +gem 'simplecov' +gem 'yard' diff --git a/Guardfile b/Guardfile index ec851c6..328f4e6 100644 --- a/Guardfile +++ b/Guardfile @@ -17,7 +17,7 @@ # # and, you'll have to watch "config/Guardfile" instead of "Guardfile" -# Note: The cmd option is now required due to the increasing number of ways +# NOTE: The cmd option is now required due to the increasing number of ways # rspec may be run, below are examples of the most common uses. # * bundler: 'bundle exec rspec' # * bundler binstubs: 'bin/rspec' diff --git a/README.md b/README.md index 132d340..c638089 100644 --- a/README.md +++ b/README.md @@ -34,34 +34,30 @@ Yaml Normalizer can be used in frameworks like Rails, Sinatra or Cuba, but it runs stand-alone as well. ## Installation - $ gem install yaml_normalizer + gem install yaml_normalizer ## Usage ### Use executable files Yaml Normalizer provides you with two executables: `yaml_normalize` and `yaml_check`. -This is how you run them in your terminal: +To check and normalize ´a_yaml_file.yml´, run: - $ yaml_check my_yaml_file.yml - $ yaml_normalize my_yaml_file.yml + yaml_check a_yaml_file.yml + yaml_normalize a_yaml_file.yml -To check the current version of yaml_normalizer type: - $ yaml_check --version +Print the installed version of Yaml Normalizer: - or + yaml_check --version - $ yaml_normalize -v +Print help messages for ´yaml_check´ and `yaml_normalize`: -To see the help message for yaml_normalizer type: - $ yaml_check --help - - or - - $ yaml_normalize -h + yaml_check --help + yaml_normalize --help ### Include Yaml Normalizer rake tasks In your Gemfile, add gem 'yaml_normalizer', require: false + In a Rails context, you might want to only add it to `:development` and `:test` groups. In your Rakefile, add @@ -77,52 +73,71 @@ This will give you two additional rake task (`rake -T`) rake yaml:check # Check if configured YAML files are normalized rake yaml:normalize # Normalize configured YAML files - ## Development - -In order to start developing with Yaml Normalizer you need to install the following dependencies: +Start developing Yaml Normalizer by installing the following dependencies: * [git](https://git-scm.com/downloads) (version control) * [Ruby](https://www.ruby-lang.org/en/documentation/installation/) (programming language) * [RubyGems](https://rubygems.org/pages/download) (package manager for Ruby) * [Bundler](http://bundler.io/) (dependency manager for RubyGems) -##### Set up Yaml Normalizer - $ git clone git@github.com:Sage/yaml_normalizer.git - $ cd yaml_normalizer/ - $ bundle install - $ rake +### Set up Yaml Normalizer + git clone git@github.com:Sage/yaml_normalizer.git + cd yaml_normalizer/ + bundle install + +### List available rake tasks + bundle exec rake -T +``` +rake ci # Continuous integration test suite (DEFAULT) +rake flog # Analyze for code complexity in: lib +rake inch # Check documentation coverage +rake mutant # Mutation testing to check mutation coverage of current RSpec test suite +rake rubocop # Run RuboCop +rake rubocop:autocorrect # Autocorrect RuboCop offenses (only when it's safe) +rake rubocop:autocorrect_all # Autocorrect RuboCop offenses (safe and unsafe) +rake spec # Run RSpec code examples +rake yard # Generate YARD Documentation +``` + +### Generate documentation locally using Yard + bundle exec rake yard +After generating yard documentation, open generated index.html located in `doc/` in your browser. + +### Test documentation quality using Inch + bundle exec rake inch + inch list --all +This task applies static code analysis to measure documentation quality. Inch also suggests improvements. -#### Generate Documentation -##### Using Yard - $ bundle exec rake yard -After generating yard documentation, open index.html at doc/ in your browser. +### Run RSpec + bundle exec rake spec + rspec +[RSpec](https://rubygems.org/gems/rspec) is a testing framework for Ruby. Running this task executes all tests located in `spec/`. -#### Test Implementation -##### Run continuous integration test suite - $ bundle exec rake -Running this task executes the following tasks: `inch` `rubocop` `ci_flog` `ci_spec` `mutant` +### Check and Correct static code metrics using Rubocop + bundle exec rake rubocop + rubocop -#### Test Documentation using Inch - $ bundle exec rake inch -This task applies static code analysis to measure documentation quality. Inch also suggests improvements. + bundle exec rake rubocop:autocorrect + rubocop -a -##### Run Guard - $ bundle exec guard -Guard keeps track of file changes and automatically runs the unit tests related to changed files. + bundle exec rake rubocop:autocorrect_all + rubocop -A +[Rubocop](https://rubygems.org/gems/rubocop) is a static code analyzer that checks for code quality and style issues. -##### Run RSpec - $ bundle exec rake spec -This task runs the full unit test suite based on RSpec. +### Run Flog (check complexity) + bundle exec rake flog +[Flog](https://rubygems.org/gems/flog) is a static code analyzer that checks for code complexity. -#### Test Tests using mutant - $ bundle exec rake mutant +### Run Guard + bundle exec guard +[Guard](https://rubygems.org/gems/guard) keeps track of file changes and automatically runs tests related to updated files. -#### Check and Correct static code metrics using Rubocop - $ bundle exec rake rubocop - $ bundle exec rake rubocop:auto_correct +### Run continuous integration test suite + bundle exec rake +Running this task executes the following tasks: ´ci_spec´ ´inch´ ´ci_flog´ ´rubocop` -#### Run Flog (check complexity) - $ bundle exec flog -a lib +### Test Tests using mutant (requires mutant-license) + bundle exec rake mutant ## Contributing Bug reports and pull requests are welcome on GitHub at diff --git a/Rakefile b/Rakefile index 6b51717..5a7ec36 100644 --- a/Rakefile +++ b/Rakefile @@ -34,7 +34,7 @@ end task :ci_spec do ci_task('CI spec', 'bundle exec rspec 2>&1') do |out, success| out_lines = out.split("\n") - success &&= out_lines[-3].match?(/ 0 failures/) + success &&= out_lines[-3].include?(' 0 failures') success &&= out_lines[-1].match?(/LOC \(100\.0%\) covered\.$/) success end @@ -42,14 +42,10 @@ end desc 'Mutation testing to check mutation coverage of current RSpec test suite' task :mutant do - mutant_sh = 'bundle exec mutant \ - --since 0baa016e8a89b81b35e74ff988594dc11fbd48f5 \ - --include lib \ - --require yaml_normalizer \ - --use rspec YamlNormalizer* 2>&1' + mutant_sh = 'bundle exec mutant run 2>&1' ci_task('mutant', mutant_sh) do |out, success| - success && out.split("\n").any? { |l| l == 'Coverage: 100.00%' } + success && out.split("\n").any?('Coverage: 100.00%') end end @@ -66,6 +62,6 @@ FlogTask.new do |config| end desc 'Continuous integration test suite (DEFAULT)' -task ci: %i[inch rubocop ci_flog ci_spec mutant] +task ci: %i[ci_spec inch ci_flog rubocop] task default: :ci diff --git a/lib/yaml_normalizer/ext/sort_by_key.rb b/lib/yaml_normalizer/ext/sort_by_key.rb index 5ba78c5..274109d 100644 --- a/lib/yaml_normalizer/ext/sort_by_key.rb +++ b/lib/yaml_normalizer/ext/sort_by_key.rb @@ -16,7 +16,7 @@ module SortByKey # => {:a=>nil, :b=>{:x=>10, :y=>{:a=>2, :b=>1}, :z=>20}} # @param recursive [Boolean] defines if sort_by_key is called on child # nodes, defaults to true - def sort_by_key(recursive = true) + def sort_by_key(recursive: true) keys.sort_by(&:to_s).each_with_object({}) do |key, seed| value = seed[key] = fetch(key) seed[key] = value.extend(SortByKey).sort_by_key if recursive && value.instance_of?(Hash) diff --git a/lib/yaml_normalizer/helpers/normalize.rb b/lib/yaml_normalizer/helpers/normalize.rb index bed88af..ab3e078 100644 --- a/lib/yaml_normalizer/helpers/normalize.rb +++ b/lib/yaml_normalizer/helpers/normalize.rb @@ -23,8 +23,10 @@ module Normalize # @return [String] normalized YAML string def normalize_yaml(yaml) hashes = parse(yaml).transform - hashes.each { |hash| hash.extend(Ext::SortByKey) } - hashes.map(&:sort_by_key).map(&:to_yaml).join + hashes.map do |hash| + hash.extend(Ext::SortByKey) + hash.sort_by_key.to_yaml + end.join end private @@ -39,7 +41,7 @@ def read(file) def relative_path_for(file) realpath = Pathname.new(file).realpath - realpath.relative_path_from(Pathname.new(Dir.pwd)) + realpath.relative_path_from(Dir.pwd) end end end diff --git a/lib/yaml_normalizer/helpers/param_parser.rb b/lib/yaml_normalizer/helpers/param_parser.rb index 97e7633..0d2d60d 100644 --- a/lib/yaml_normalizer/helpers/param_parser.rb +++ b/lib/yaml_normalizer/helpers/param_parser.rb @@ -12,23 +12,23 @@ module ParamParser def parse_params(*args) OptionParser.new do |opts| opts.banner = "Usage: #{program_name} [options] file1, file2..." - opts.on('-v', '--version', 'Prints the yaml_normalizer version') { print_version } opts.on('-h', '--help', 'Prints this help') { print_help(opts) } + opts.on('-v', '--version', 'Prints the yaml_normalizer version') { print_version } end.parse(args) end - # Print current version of the tool - def print_version - print("#{YamlNormalizer::VERSION}\n") - exit_success - end - # Print current version of the tool # @param [Option] opts - options of opt_parser object # @return nil def print_help(opts) print(opts) - exit_success + exit + end + + # Print current version of the tool + def print_version + print("#{VERSION}\n") + exit end private @@ -36,10 +36,6 @@ def print_help(opts) def program_name $PROGRAM_NAME.split('/').last end - - def exit_success - exit unless ENV['ENV'] == 'test' - end end end end diff --git a/lib/yaml_normalizer/rake_task.rb b/lib/yaml_normalizer/rake_task.rb index 0cf1ab3..1694bcc 100644 --- a/lib/yaml_normalizer/rake_task.rb +++ b/lib/yaml_normalizer/rake_task.rb @@ -8,35 +8,34 @@ module YamlNormalizer # Provides Rake task integration class RakeTask < ::Rake::TaskLib - # The name of the task - # @return [String] name of the Rake task + # The name of the rake task + # @return [String] name of the rake task attr_accessor :name # The YAML files to process. - # @example Task files assignment + # @example # YamlNormalizer::RakeTask.new do |task| # task.files = ['config/locale/*.yml', 'config/*.yml'] # end # @return [Array] a list of file globing Strings attr_accessor :files - # Create a YamlNormalizer rake task object. - # Use this to - # @example - # In your Rakefile, add: - # YamlNormalizer::RakeTask.new + # Add YAML Normalizer rake tasks to your project by adding the following line to your Rakefile: + # YamlNormalizer::RakeTask.new # - # To be more specific, configure YAML Normalizer's mode and files like so: - # YamlNormalizer::RakeTask.new do |config| - # config.files = Dir[File.join(File.dirname(__FILE__), 'include.yml')] - # end + # This gives you the following tasks (run rake -T) + # rake yaml:check # Check if configured YAML files are normalized + # rake yaml:normalize # Normalize configured YAML files + # + # @example Configure YAML Normalizer's rake task + # YamlNormalizer::RakeTask.new do |config| + # config.files = Dir[File.join(File.dirname(__FILE__), 'include.yml')] + # end # - # This gives you the following tasks (run rake -T) - # rake yaml:check # Check if given YAML are normalized - # rake yaml:normalize # Normalize given YAML files - # @param name [String] name of the Rake task + # @param name [String] name of the rake task # @param &block [Proc] optional, evaluated inside the task definition def initialize(name = 'yaml', &block) + super() yield(self) if block desc 'Check if configured YAML files are normalized' diff --git a/lib/yaml_normalizer/services/check.rb b/lib/yaml_normalizer/services/check.rb index b120757..71923e2 100644 --- a/lib/yaml_normalizer/services/check.rb +++ b/lib/yaml_normalizer/services/check.rb @@ -20,6 +20,7 @@ class Check < Base # more Strings that are interpreted as file glob pattern. # @param *args [Array] a list of file glob patterns def initialize(*args) + super parse_params(*args) files = args.each_with_object([]) { |a, o| o << Dir[a.to_s] } @files = files.flatten.sort.uniq @@ -37,7 +38,7 @@ def call def process(file) return true if IsYaml.call(file) && normalized?(file) - $stderr.print "#{file} not a YAML file\n" + $stderr.print "#{file} is not a YAML file\n" nil end diff --git a/lib/yaml_normalizer/services/is_yaml.rb b/lib/yaml_normalizer/services/is_yaml.rb index 3521091..ce1e944 100644 --- a/lib/yaml_normalizer/services/is_yaml.rb +++ b/lib/yaml_normalizer/services/is_yaml.rb @@ -19,6 +19,7 @@ class IsYaml < Base # String. # @param file [String] file path to be regarded def initialize(file) + super @file = file.to_s end diff --git a/lib/yaml_normalizer/services/normalize.rb b/lib/yaml_normalizer/services/normalize.rb index c97a7e6..6ce97ee 100644 --- a/lib/yaml_normalizer/services/normalize.rb +++ b/lib/yaml_normalizer/services/normalize.rb @@ -20,6 +20,7 @@ class Normalize < Base # more String that are interpreted as file glob pattern. # @param *args [Array] a list of file glob patterns def initialize(*args) + super parse_params(*args) files = args.each_with_object([]) { |a, o| o << Dir[a.to_s] } @files = files.flatten.sort.uniq @@ -43,7 +44,7 @@ def process(file) def normalize!(file) file = relative_path_for(file) if stable?(input = read(file), norm = normalize_yaml(input)) - File.open(file, 'w') { |f| f.write(norm) } + File.write(file, norm) $stderr.print "[NORMALIZED] #{file}\n" else $stderr.print "[ERROR] Could not normalize #{file}\n" diff --git a/spec/ci_helper.rb b/spec/ci_helper.rb index 1bc1719..d475f68 100644 --- a/spec/ci_helper.rb +++ b/spec/ci_helper.rb @@ -24,7 +24,7 @@ def ci_task(name, cmd) # @param cmd [String] command to be executed in child process # @param tee [Boolean] true if output should be printed (default) # rubocop:disable Metrics/MethodLength -def ci_run(cmd, tee = true) +def ci_run(cmd, tee: true) time = Time.new output = StringIO.new IO.popen(cmd) do |f| diff --git a/spec/data/valid2.yml b/spec/data/valid2.yml index 755a62f..78b283b 100644 --- a/spec/data/valid2.yml +++ b/spec/data/valid2.yml @@ -1,8 +1,10 @@ ---- +--- this: is a: - valid - yaml - file +having: + namespaced: values --- that: one, too diff --git a/spec/data/valid2_normalized.yml b/spec/data/valid2_normalized.yml index d35701f..f6446f9 100644 --- a/spec/data/valid2_normalized.yml +++ b/spec/data/valid2_normalized.yml @@ -1,4 +1,4 @@ ---- +--- a: - valid - yaml diff --git a/spec/data/valid_bom.yml b/spec/data/valid_bom.yml new file mode 100644 index 0000000..755a62f --- /dev/null +++ b/spec/data/valid_bom.yml @@ -0,0 +1,8 @@ +--- +this: is +a: +- valid +- yaml +- file +--- +that: one, too diff --git a/spec/nested_spec.rb b/spec/nested_spec.rb deleted file mode 100644 index d278b39..0000000 --- a/spec/nested_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -# rubocop:disable Metrics/BlockLength -RSpec.describe YamlNormalizer::Ext::Nested do - context 'extended Hash instances with "nested"' do - subject { hash.extend(described_module).nested } - let(:described_module) { described_class } - let(:hash) do - { 'a.b.c' => 1, 'b.x' => 2, - 'b.y.one' => true, - 'b.y.two' => nil, - 'no_dot' => 'ok', - 3 => String, - sym: 'ok', - nil => 'nil', - '' => 'empty' } - end - - it 'does not modify the original object' do - expect { subject }.to_not(change { hash }) - end - - it 'converts a Hash from a flat key-value pairs to a tree structure' do - expect(subject).to eql('a' => { 'b' => { 'c' => 1 } }, - 'b' => { 'x' => 2, - 'y' => { 'one' => true, 'two' => nil } }, - 'no_dot' => 'ok', - '3' => String, - 'sym' => 'ok', - '' => 'empty') - end - - it 'resets the default_proc' do - expect(subject[:unknown]).to be_nil - end - - it 'does not create unknown keys on access' do - expect(subject.dig('a', 'unknown')).to be_nil - end - end - - it 'does not modify Ruby Core class Hash' do - expect { {}.nested }.to raise_error(NoMethodError) - end -end -# rubocop:enable Metrics/BlockLength diff --git a/spec/rake_task_spec.rb b/spec/rake_task_spec.rb deleted file mode 100644 index ba0fae1..0000000 --- a/spec/rake_task_spec.rb +++ /dev/null @@ -1,114 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'yaml_normalizer/rake_task' -Rake::TaskManager.record_task_metadata = true - -# rubocop:disable Metrics/BlockLength -RSpec.describe YamlNormalizer::RakeTask do - describe 'defining tasks' do - context 'yaml:check' do - it 'creates "yaml:check"' do - subject - expect(Rake::Task.task_defined?('yaml:check')).to be true - end - - it 'describes "yaml:check"' do - expect(Rake::Task['yaml:check'].comment) - .to eql('Check if configured YAML files are normalized') - end - - it 'lists "yaml:normalize"' do - subject - expect(Rake::Task.task_defined?('yaml:normalize')).to be true - end - - it 'describes "yaml:normalize"' do - expect(Rake::Task['yaml:normalize'].comment) - .to eql('Normalize configured YAML files') - end - - it 'creates a named task' do - described_class.new(:lint_lib) - - expect(Rake::Task.task_defined?('lint_lib:check')).to be true - end - - it 'creates a named task' do - described_class.new(:lint_lib) - - expect(Rake::Task.task_defined?('lint_lib:normalize')).to be true - end - end - end - - context 'running rake tasks' do - around do |example| - $stdout = StringIO.new - $stderr = StringIO.new - Rake::Task[task].clear if Rake::Task.task_defined?(task) - - example.run - - $stdout = STDOUT - $stderr = STDERR - end - - describe 'running rake task "yaml:check"' do - let(:task) { 'yaml:check' } - - it 'runs with default file configuration' do - described_class.new - - expect(YamlNormalizer::Services::Check).to receive(:call) - .with(no_args) - .and_return(true) - Rake::Task['yaml:check'].execute - end - - it 'runs with files = "*.yml"' do - described_class.new { |task| task.files = '*.yml' } - - expect(YamlNormalizer::Services::Check).to receive(:call) - .with('*.yml') - .and_return(true) - Rake::Task['yaml:check'].execute - end - - it 'raises an error if yaml:check fails' do - described_class.new - msg = <<~MSG - yaml:check failed. - Run 'rake yaml:normalize' to normalize YAML files. - MSG - - allow(YamlNormalizer::Services::Check).to receive(:call).with(no_args) - - expect { Rake::Task['yaml:check'].execute }.to raise_error(msg) - end - end - - describe 'running rake task "yaml:normalize"' do - let(:task) { 'yaml:normalize' } - - it 'runs with default options' do - described_class.new - - expect(YamlNormalizer::Services::Normalize) - .to receive(:call).with(no_args) - - Rake::Task[task].execute - end - - it 'runs with configured files' do - described_class.new { |task| task.files = '*.yml' } - - expect(YamlNormalizer::Services::Normalize) - .to receive(:call).with('*.yml') - - Rake::Task[task].execute - end - end - end -end -# rubocop:enable Metrics/BlockLength diff --git a/spec/services/base_spec.rb b/spec/services/base_spec.rb deleted file mode 100644 index 91c0852..0000000 --- a/spec/services/base_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -# rubocop:disable Metrics/BlockLength -RSpec.describe YamlNormalizer::Services::Base do - subject { described_class } - let(:args) { [:foo, 'bar', 842] } - - describe '.new' do - it 'accepts arbitrary arguments' do - instance = subject.new(*args) - expect(instance.instance_variable_get(:@args)).to eql(args) - end - end - - describe '.call' do - let(:args) { [:foo, 'bar', 842] } - let(:instance) { double('SubClassOfBase', call: nil) } - - it 'creates an instance and passes all arguments' do - expect(subject).to receive(:new).with(*args).and_return(instance) - subject.call(*args) - end - - it 'calls method "call" on instance' do - allow(subject).to receive(:new).with(*args).and_return(instance) - expect(instance).to receive(:call) - subject.call(*args) - end - end - - describe '#call' do - subject { described_class.new } - it 'raises NotImplementedError' do - expect { subject.call }.to raise_error(NotImplementedError) - end - end -end -# rubocop:enable Metrics/BlockLength diff --git a/spec/services/check_spec.rb b/spec/services/check_spec.rb deleted file mode 100644 index 6361684..0000000 --- a/spec/services/check_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -# rubocop:disable Metrics/BlockLength -RSpec.describe YamlNormalizer::Services::Check do - let(:path) { "#{SpecConfig.data_path}#{File::SEPARATOR}" } - let(:args) { ["#{path}#{name}"] } - - include_examples :args_receving_service - - context 'parially invalid globbing inputs' do - subject { described_class.new(*args) } - let(:args) { ["#{path}*.1", :invalid, "#{path}1.*"] } - it 'sanitaizes list of files before processing' do - expect(subject.files).to eql ["#{path}1.1", "#{path}1.2", "#{path}2.1"] - end - end - - describe '#call' do - subject { described_class.new(*args).call } - - context 'only invalid globbing inputs' do - let(:args) { ['lol', :foo, nil] } - it { is_expected.to eql(true) } - end - - context 'invalid YAML file' do - let(:name) { 'invalid.yml' } - - it { is_expected.to eql(false) } - - it 'prints "not a YAML file" message to STDERR' do - expect { subject } - .to output("#{path}#{name} not a YAML file\n").to_stderr - end - end - - context 'file handling' do - let(:data) { { path: nil } } - let(:file) { data[:path] } - let(:args) { file.path } - - around :example do |example| - Tempfile.open(name) do |yaml| - yaml.write(File.read(path + name)) - yaml.rewind - data[:path] = yaml - example.run - end - end - - context 'single-document YAML file' do - context 'denormalized YAML file' do - let(:name) { 'valid.yml' } - - it { is_expected.to eql(false) } - - it 'prints out an error message with relative file path' do - f_abs = Pathname.new(file.path).realpath - f = f_abs.relative_path_from(Pathname.new(Dir.pwd)) - expect { subject } - .to output("[FAILED] normalization suggested for #{f}\n") - .to_stdout - end - end - - context 'normalized YAML file' do - let(:name) { 'valid_normalized.yml' } - - it { is_expected.to eql(true) } - - it 'prints out a success message with relative file path' do - f_abs = Pathname.new(file.path).realpath - f = f_abs.relative_path_from(Pathname.new(Dir.pwd)) - expect { subject } - .to output("[PASSED] already normalized #{f}\n").to_stdout - end - end - end - - context 'multi-document YAML file' do - let(:name) { 'valid2_normalized.yml' } - - it 'passes if YAML file is already normalized' do - f_abs = Pathname.new(file.path).realpath - f = f_abs.relative_path_from(Pathname.new(Dir.pwd)) - expect { subject } - .to output("[PASSED] already normalized #{f}\n").to_stdout - end - end - end - end -end -# rubocop:enable Metrics/BlockLength diff --git a/spec/services/is_yaml_spec.rb b/spec/services/is_yaml_spec.rb deleted file mode 100644 index 06691dc..0000000 --- a/spec/services/is_yaml_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe YamlNormalizer::Services::IsYaml do - describe '#call' do - subject { described_class.new(file).call } - - let(:path) { "#{SpecConfig.data_path}#{File::SEPARATOR}" } - - context 'not a string, not a file' do - let(:file) { :does_not_exist } - it { is_expected.to eql false } - end - - context 'invalid YAML file' do - let(:file) { "#{path}invalid.yml" } - it { is_expected.to eql false } - end - - context 'scalar YAML file' do - let(:file) { "#{path}scalar.yml" } - it { is_expected.to eql false } - end - - context 'valid YAML file' do - let(:file) { "#{path}valid.yml" } - it { is_expected.to eql true } - end - end -end diff --git a/spec/services/normalize_spec.rb b/spec/services/normalize_spec.rb deleted file mode 100644 index a5a3786..0000000 --- a/spec/services/normalize_spec.rb +++ /dev/null @@ -1,123 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -# rubocop:disable Metrics/BlockLength -RSpec.describe YamlNormalizer::Services::Normalize do - subject { described_class.new(*args) } - - let(:path) { "#{SpecConfig.data_path}#{File::SEPARATOR}" } - let(:args) { ["#{path}#{file}"] } - - include_examples :args_receving_service - - context 'invalid args, no arg matches file' do - subject { described_class.new(*args).call } - let(:args) { ['lol', :foo, nil] } - it { expect { -> { subject } }.to_not raise_error } - it { is_expected.to eql [] } - end - - context 'partially invalid args' do - let(:args) { ["#{path}*.1", :invalid, "#{path}1.*"] } - it 'sanitaizes list of files before processing' do - expect(subject.files).to eql ["#{path}1.1", "#{path}1.2", "#{path}2.1"] - end - end - - describe '#call' do - subject { described_class.new(*args).call } - - context 'invalid YAML file' do - let(:file) { 'invalid.yml' } - it 'prints "not a YAML file" message to STDERR' do - expect { subject } - .to output("#{path}invalid.yml not a YAML file\n").to_stderr - end - end - - context 'using relative path' do - it 'processes files with a relative path' do - Tempfile.open('foo') do |yaml| - Dir.chdir(Pathname(yaml).dirname) - expect { described_class.new(Pathname(yaml).basename).call } - .to_not raise_error - end - end - end - - context 'single-document YAML file' do - let(:file) { 'valid.yml' } - let(:expected) { 'valid_normalized.yml' } - - it 'normalizes and updates the yaml file' do - Tempfile.open(file) do |yaml| - yaml.write(File.read(path + file)) - yaml.rewind - expect do - stderr = $stderr - $stderr = StringIO.new - described_class.new(yaml.path).call - $stderr = stderr - end.to( - change { File.read(yaml.path) } - .from(File.read("#{path}#{file}")) - .to(File.read("#{path}#{expected}")) - ) - end - end - - it 'prints out a success message with relative file path' do - Tempfile.open(file) do |yaml| - yaml.write(File.read(path + file)) - yaml.rewind - f_abs = Pathname.new(yaml.path).realpath - f = f_abs.relative_path_from(Pathname.new(Dir.pwd)) - expect { described_class.new(yaml.path).call } - .to output("[NORMALIZED] #{f}\n").to_stderr - end - end - end - - context 'multi-document YAML file' do - let(:file) { 'valid2.yml' } - - it 'normalizes the yaml file' do - Tempfile.open(file) do |yaml| - yaml.write(File.read(path + file)) - yaml.rewind - f_abs = Pathname.new(yaml.path).realpath - f = f_abs.relative_path_from(Pathname.new(Dir.pwd)) - expect { described_class.new(yaml.path).call } - .to output("[NORMALIZED] #{f}\n").to_stderr - end - end - end - - context 'not stable' do - let(:file) { 'valid.yml' } - let(:other) { 'valid_normalized.yml' } - let(:defect) { [{ error: nil }.extend(YamlNormalizer::Ext::Namespaced)] } - - it 'prints out an error to STDERR' do - Tempfile.open(file) do |yaml| - yaml.write(File.read(path + file)) - yaml.rewind - normalize = described_class.new(yaml.path) - - allow(normalize).to receive(:convert) - .with(File.read(path + file)).and_call_original - allow(normalize).to receive(:convert) - .with(File.read(path + other)).and_return(defect) - - f_abs = Pathname.new(yaml.path).realpath - f = f_abs.relative_path_from(Pathname.new(Dir.pwd)) - expect { normalize.call } - .to output("[ERROR] Could not normalize #{f}\n") - .to_stderr - end - end - end - end -end -# rubocop:enable Metrics/BlockLength diff --git a/spec/shared_examples/param_parser_shared_example.rb b/spec/shared_examples/param_parser_shared_example.rb index 4dc0ea8..9092a60 100644 --- a/spec/shared_examples/param_parser_shared_example.rb +++ b/spec/shared_examples/param_parser_shared_example.rb @@ -1,32 +1,48 @@ # frozen_string_literal: true -shared_examples :args_receving_service do +RSpec.shared_examples 'args receiving service' do describe 'param triggered methods' do subject { described_class.new(*args) } - context 'when presented with version param' do + context 'when called with version param' do ['-v', '--version'].each do |param| let(:args) { [param] } - it 'prints the version of software' do + # rubocop:disable RSpec/MultipleExpectations + it 'prints the version and exits' do expect do - subject.call + expect do + subject.call + end.to raise_error(SystemExit) end.to output("#{YamlNormalizer::VERSION}\n").to_stdout end + # rubocop:enable RSpec/MultipleExpectations end end - context 'when presented with help param' do + context 'when called with help param' do ['-h', '--help'].each do |param| let(:args) { [param] } - it 'prints the help message' do + let(:help_message) do + "Usage: #{program_name} [options] file1, file2...\n " \ + "-h, --help Prints this help\n " \ + "-v, --version Prints the yaml_normalizer version\n" + end + + # rubocop:disable RSpec/MultipleExpectations + it 'prints the help message and exits' do expect do - subject.call - end.to output("Usage: rspec [options] file1, file2...\n"\ - " -v, --version Prints the yaml_normalizer version\n"\ - " -h, --help Prints this help\n").to_stdout + expect do + subject.call + end.to raise_error(SystemExit) + end.to output(help_message).to_stdout end + # rubocop:enable RSpec/MultipleExpectations end end + + def program_name + $PROGRAM_NAME.split('/').last + end end end diff --git a/spec/sort_by_key_spec.rb b/spec/sort_by_key_spec.rb deleted file mode 100644 index d85cae8..0000000 --- a/spec/sort_by_key_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -# rubocop:disable Metrics/BlockLength -RSpec.describe YamlNormalizer::Ext::SortByKey do - context 'extended Hash instances with "sort_by_key"' do - subject { hash.extend(described_module).sort_by_key(recursive) } - let(:described_module) { described_class } - let(:recursive) { true } - let(:hash) { { b: { z: 20, x: 10, y: { b: 1, a: 2 } }, a: nil } } - let(:expected) { { a: nil, b: { x: 10, y: { a: 2, b: 1 }, z: 20 } } } - - it 'does not modify the original object' do - expect { subject }.to_not(change { hash }) - end - - context 'keys of different types' do - let(:hash) { { 1 => nil, 'two': :ok, false => {} } } - let(:expected) { { 1 => nil, false => {}, 'two': :ok } } - it 'sorts objects by their String representation' do - expect(subject.inspect).to eql(expected.inspect) - end - end - - context 'first level only' do - let(:recursive) { false } - let(:expected) { { a: nil, b: { z: 20, x: 10, y: { b: 1, a: 2 } } } } - it 'sorts first level keys only' do - expect(subject.inspect).to eql(expected.inspect) - end - end - - context 'recursive' do - it 'sorts keys of all levels' do - expect(subject.inspect).to eql(expected.inspect) - end - end - end - - it 'does not modify Ruby Core class Hash' do - expect { {}.sort_by_key }.to raise_error(NoMethodError) - end -end -# rubocop:enable Metrics/BlockLength diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ee40cd7..4b3d20b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -38,18 +38,4 @@ def data_path mocks.verify_partial_doubles = true end config.disable_monkey_patching! - - original_stderr = $stderr - original_stdout = $stdout - - config.before(:all) do - # Redirect stderr and stdout - $stderr = File.open(File::NULL, "w") - $stdout = File.open(File::NULL, "w") - end - - config.after(:all) do - $stderr = original_stderr - $stdout = original_stdout - end end diff --git a/spec/namespaced_spec.rb b/spec/yaml_normalizer/ext/namespaced_spec.rb similarity index 63% rename from spec/namespaced_spec.rb rename to spec/yaml_normalizer/ext/namespaced_spec.rb index 913032d..4b36add 100644 --- a/spec/namespaced_spec.rb +++ b/spec/yaml_normalizer/ext/namespaced_spec.rb @@ -3,17 +3,18 @@ require 'spec_helper' RSpec.describe YamlNormalizer::Ext::Namespaced do - context 'extended Hash instances with "namespaced"' do - subject { hash.extend(described_module).namespaced } + context 'when extending a Hash instances with Ext::Namespaced' do + subject(:namespaced) { hash.extend(described_module).namespaced } + let(:described_module) { described_class } let(:hash) { { b: { z: 20, x: 10 }, a: nil } } it 'does not modify the original object' do - expect { subject }.to_not(change { hash }) + expect { namespaced }.not_to(change { hash }) end it 'converts a Hash from a tree structure to a plain key-value' do - expect(subject).to eql('b.z' => 20, 'b.x' => 10, 'a' => nil) + expect(namespaced).to eql('b.z' => 20, 'b.x' => 10, 'a' => nil) end end diff --git a/spec/yaml_normalizer/ext/nested_spec.rb b/spec/yaml_normalizer/ext/nested_spec.rb new file mode 100644 index 0000000..379be7d --- /dev/null +++ b/spec/yaml_normalizer/ext/nested_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe YamlNormalizer::Ext::Nested do + context 'when extending Hash instances with Ext::Nested' do + subject(:nested) { hash.extend(described_module).nested } + + let(:described_module) { described_class } + let(:hash) do + { 'a.b.c' => 1, 'b.x' => 2, + 'b.y.one' => true, 'b.y.two' => nil, + 'no_dot' => 'ok', 3 => String, sym: 'ok', nil => 'nil', '' => 'empty' } + end + + it 'does not modify the original object' do + expect { nested }.not_to(change { hash }) + end + + it 'converts a Hash from a flat key-value pairs to a tree structure' do + expect(nested).to eql('a' => { 'b' => { 'c' => 1 } }, + 'b' => { 'x' => 2, 'y' => { 'one' => true, 'two' => nil } }, + 'no_dot' => 'ok', '3' => String, 'sym' => 'ok', '' => 'empty') + end + + it 'resets the default_proc' do + expect(nested[:unknown]).to be_nil + end + + it 'does not create unknown keys on access' do + expect(nested.dig('a', 'unknown')).to be_nil + end + end + + it 'does not modify Ruby Core class Hash' do + expect { {}.nested }.to raise_error(NoMethodError) + end +end diff --git a/spec/yaml_normalizer/ext/sort_by_key_spec.rb b/spec/yaml_normalizer/ext/sort_by_key_spec.rb new file mode 100644 index 0000000..6aaf658 --- /dev/null +++ b/spec/yaml_normalizer/ext/sort_by_key_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe YamlNormalizer::Ext::SortByKey do + context 'when extending a Hash with Ext::SortByKey' do + subject(:sorted_hash) { hash.extend(described_module).sort_by_key(recursive: recursive) } + + let(:described_module) { described_class } + let(:recursive) { true } + let(:hash) { { b: { z: 20, x: 10, y: { b: 1, a: 2 } }, a: nil } } + let(:expected) { { a: nil, b: { x: 10, y: { a: 2, b: 1 }, z: 20 } } } + + it 'does not modify the original object' do + expect { sorted_hash }.not_to(change { hash }) + end + + context 'when keys have different types' do + let(:hash) { { 1 => nil, two: :ok, false => {} } } + let(:expected) { { 1 => nil, false => {}, two: :ok } } + + it 'sorts objects by their String representation' do + expect(sorted_hash.inspect).to eql(expected.inspect) + end + end + + context 'when sorting by keys in first level only' do + let(:recursive) { false } + let(:expected) { { a: nil, b: { z: 20, x: 10, y: { b: 1, a: 2 } } } } + + it 'sorts the Hash but not its nested Hashes' do + expect(sorted_hash.inspect).to eql(expected.inspect) + end + end + + context 'when sorting by key recursively' do + it 'sorts the Hash and all nested Hashes' do + expect(sorted_hash.inspect).to eql(expected.inspect) + end + end + end + + it 'does not modify Ruby Core class Hash' do + expect { {}.sort_by_key }.to raise_error(NoMethodError) + end +end diff --git a/spec/yaml_normalizer/rake_task_spec.rb b/spec/yaml_normalizer/rake_task_spec.rb new file mode 100644 index 0000000..6d448a8 --- /dev/null +++ b/spec/yaml_normalizer/rake_task_spec.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'yaml_normalizer/rake_task' +Rake::TaskManager.record_task_metadata = true + +module YamlNormalizer + RSpec.describe RakeTask do + describe 'defining tasks' do + describe 'yaml:check' do + it 'creates "yaml:check"' do + described_class.new + expect(Rake::Task.task_defined?('yaml:check')).to be true + end + + it 'describes "yaml:check"' do + expect(Rake::Task['yaml:check'].comment) + .to eql('Check if configured YAML files are normalized') + end + + it 'creates a named task' do + described_class.new(:lint_lib) + + expect(Rake::Task.task_defined?('lint_lib:check')).to be true + end + end + + describe 'yaml:normalize' do + it 'lists "yaml:normalize"' do + described_class.new + expect(Rake::Task.task_defined?('yaml:normalize')).to be true + end + + it 'describes "yaml:normalize"' do + expect(Rake::Task['yaml:normalize'].comment) + .to eql('Normalize configured YAML files') + end + + it 'creates a named task' do + described_class.new(:lint_lib) + + expect(Rake::Task.task_defined?('lint_lib:normalize')).to be true + end + end + end + + describe 'running rake tasks' do + around do |example| + Rake::Task[task].clear if Rake::Task.task_defined?(task) + example.run + end + + describe 'running rake task "yaml:check"' do + let(:task) { 'yaml:check' } + let(:msg) do + <<~MSG + yaml:check failed. + Run 'rake yaml:normalize' to normalize YAML files. + MSG + end + + it 'runs with default file configuration' do + described_class.new + allow(Services::Check).to receive(:call).and_return(true) + + Rake::Task['yaml:check'].execute + + expect(Services::Check).to have_received(:call).with(no_args) + end + + it 'runs with files = "*.yml"' do + described_class.new { |task| task.files = '*.yml' } + allow(Services::Check).to receive(:call).and_return(true) + + Rake::Task['yaml:check'].execute + + expect(Services::Check).to have_received(:call).with('*.yml') + end + + it 'raises an error if yaml:check fails' do + described_class.new + allow(Services::Check).to receive(:call).with(no_args) + allow($stderr).to receive(:write) + + expect { Rake::Task['yaml:check'].execute }.to raise_error(msg) + end + end + + describe 'running rake task "yaml:normalize"' do + let(:task) { 'yaml:normalize' } + + it 'runs with default options' do + described_class.new + allow(Services::Normalize).to receive(:call) + + Rake::Task[task].execute + + expect(Services::Normalize).to have_received(:call).with(no_args) + end + + it 'runs with configured files' do + described_class.new { |task| task.files = '*.yml' } + allow(Services::Normalize).to receive(:call) + + Rake::Task[task].execute + + expect(Services::Normalize).to have_received(:call).with('*.yml') + end + end + end + end +end diff --git a/spec/yaml_normalizer/services/base_spec.rb b/spec/yaml_normalizer/services/base_spec.rb new file mode 100644 index 0000000..3cb0b30 --- /dev/null +++ b/spec/yaml_normalizer/services/base_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# Dummy class to test base class +class Dummy < YamlNormalizer::Services::Base; end + +RSpec.describe YamlNormalizer::Services::Base do + let(:args) { [:foo, 'bar', 842] } + + describe '.new' do + it 'accepts arbitrary arguments' do + instance = described_class.new(*args) + expect(instance.instance_variable_get(:@args)).to eql(args) + end + end + + describe '.call' do + let(:args) { [:foo, 'bar', 842] } + let(:instance) { instance_double(Dummy, call: nil) } + + it 'creates an instance and passes all arguments' do + allow(Dummy).to receive(:new).and_return(instance) + Dummy.call(*args) + expect(Dummy).to have_received(:new).with(*args) + end + + it 'calls method "call" on instance' do + allow(Dummy).to receive(:new).with(*args).and_return(instance) + Dummy.call(*args) + expect(instance).to have_received(:call) + end + end + + describe '#call' do + subject(:base_service) { described_class.new } + + it 'raises NotImplementedError' do + expect { base_service.call }.to raise_error(NotImplementedError) + end + end +end diff --git a/spec/yaml_normalizer/services/check_spec.rb b/spec/yaml_normalizer/services/check_spec.rb new file mode 100644 index 0000000..ad90ad9 --- /dev/null +++ b/spec/yaml_normalizer/services/check_spec.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe YamlNormalizer::Services::Check do + let(:path) { "#{SpecConfig.data_path}#{File::SEPARATOR}" } + let(:args) { ["#{path}#{name}"] } + + include_examples 'args receiving service' + + describe '.new' do + subject(:instance) { described_class.new(args) } + + let(:args) { ['file.yml'] } + + it 'calls super to initialize state of the parent class' do + expect(instance.instance_variable_get(:@args)).to eql([args]) + end + end + + context 'when there are both valid and invalid globbing inputs' do + subject(:check_service_instance) { described_class.new(*args) } + + let(:args) { ["#{path}*.1", :invalid, "#{path}1.*"] } + let(:expected) { ["#{path}1.1", "#{path}1.2", "#{path}2.1"] } + + it 'sanitizes list of files before processing' do + expect(check_service_instance.files).to eql expected + end + end + + describe '#call arguments' do + subject(:check_service_call) { described_class.new(*args).call } + + context 'without valid globbing inputs' do + let(:args) { ['lol', :foo, nil] } + + it { is_expected.to be(true) } + end + + context 'when YAML file is invalid' do + before { allow($stderr).to receive(:print) } + + let(:name) { 'invalid.yml' } + + it { is_expected.to be(false) } + + it 'prints "is not a YAML file" message to STDERR' do + allow($stderr).to receive(:print) + expect { check_service_call } + .to output("#{path}#{name} is not a YAML file\n").to_stderr + end + end + end + + describe '#call processing' do + subject(:check_service_call) { described_class.new(*args).call } + + let(:data) { { path: nil } } + let(:file) { data[:path] } + let(:args) { file.path } + + around do |example| + Tempfile.open(name) do |yaml| + yaml.write(File.read(path + name)) + yaml.rewind + data[:path] = yaml + example.run + end + end + + context 'when single document YAML file is denormalized' do + before do + described_class.define_method(:print) { raise 'DoNotCall' } + allow($stderr).to receive(:print) + allow($stdout).to receive(:print) + end + + let(:name) { 'valid.yml' } + + it { is_expected.to be(false) } + + it 'prints out an error message with relative file path' do + relative_path = Pathname.new(file.path).realpath.relative_path_from(Pathname.new(Dir.pwd)) + expect { check_service_call } + .to output("[FAILED] normalization suggested for #{relative_path}\n") + .to_stdout + end + end + + context 'when single document YAML file is normalized' do + before do + allow($stderr).to receive(:print) + allow($stdout).to receive(:print) + end + + let(:name) { 'valid_normalized.yml' } + + it { is_expected.to be(true) } + + it 'prints out a success message with relative file path' do + relative_path = Pathname.new(file.path).realpath.relative_path_from(Pathname.new(Dir.pwd)) + expect { check_service_call } + .to output("[PASSED] already normalized #{relative_path}\n") + .to_stdout + end + end + + context 'when YAML file contains multiple documents' do + let(:name) { 'valid2_normalized.yml' } + + it 'passes if YAML file is already normalized' do + relative_path = Pathname.new(file.path).realpath.relative_path_from(Pathname.new(Dir.pwd)) + expect { check_service_call } + .to output("[PASSED] already normalized #{relative_path}\n") + .to_stdout + end + end + end +end diff --git a/spec/yaml_normalizer/services/is_yaml_spec.rb b/spec/yaml_normalizer/services/is_yaml_spec.rb new file mode 100644 index 0000000..3fe7017 --- /dev/null +++ b/spec/yaml_normalizer/services/is_yaml_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe YamlNormalizer::Services::IsYaml do + describe '.new' do + subject(:instance) { described_class.new(file) } + + let(:file) { 'foo.yml' } + + it 'calls super to initialize state of the parent class' do + expect(instance.instance_variable_get(:@args)).to eql([file]) + end + end + + describe '#call' do + subject { described_class.new(file).call } + + let(:path) { "#{SpecConfig.data_path}#{File::SEPARATOR}" } + + context 'when passing an invalid argument' do + let(:file) { :does_not_exist } + + it { is_expected.to be false } + end + + context 'when YAML file is invalid' do + let(:file) { "#{path}invalid.yml" } + + it { is_expected.to be false } + end + + context 'when YAML file is a scalar' do + let(:file) { "#{path}scalar.yml" } + + it { is_expected.to be false } + end + + context 'when YAML file is valid' do + let(:file) { "#{path}valid.yml" } + + it { is_expected.to be true } + end + end +end diff --git a/spec/yaml_normalizer/services/normalize_spec.rb b/spec/yaml_normalizer/services/normalize_spec.rb new file mode 100644 index 0000000..a38bceb --- /dev/null +++ b/spec/yaml_normalizer/services/normalize_spec.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe YamlNormalizer::Services::Normalize do + subject(:normalize_service) { described_class.new(*args) } + + let(:path) { "#{SpecConfig.data_path}#{File::SEPARATOR}" } + let(:args) { ["#{path}#{file}"] } + + include_examples 'args receiving service' + + describe '.new' do + subject(:instance) { described_class.new(args) } + + let(:args) { ['file.yml'] } + + it 'calls super to initialize state of the parent class' do + expect(instance.instance_variable_get(:@args)).to eql([args]) + end + end + + context 'when args are partially invalid' do + let(:args) { ["#{path}*.1", :invalid, "#{path}1.*"] } + + it 'sanitizes the list of files before processing' do + expect(normalize_service.files).to eql ["#{path}1.1", "#{path}1.2", "#{path}2.1"] + end + end + + describe '#call arguments' do + subject(:normalize_call) { described_class.new(*args).call } + + context "when args don't match any file" do + let(:args) { ['lol', :foo, nil] } + + it { expect { -> { normalize_call } }.not_to raise_error } + it { is_expected.to eql [] } + end + + context 'when YAML file is invalid' do + let(:file) { 'invalid.yml' } + + it 'prints "not a YAML file" message to STDERR' do + expect { normalize_call } + .to output("#{path}invalid.yml not a YAML file\n").to_stderr + end + end + + context 'when using relative path' do + before do + allow($stderr).to receive(:print) + end + + it 'processes files with a relative path' do + Tempfile.open('foo') do |yaml| + Dir.chdir(Pathname(yaml).dirname) + + expect { described_class.new(Pathname(yaml).basename).call } + .not_to raise_error + end + end + end + end + + describe '#call processing' do + subject(:normalize_call) { described_class.new(*args).call } + + let(:tempfile) { Struct.new(:file).new } + let(:yaml) { tempfile.file } + + around do |example| + Tempfile.open(file) do |yaml| + yaml.write(File.read(path + file)) + yaml.rewind + tempfile.file = yaml + + example.run + end + end + + context 'when YAML file contains a single document' do + let(:file) { 'valid.yml' } + + it 'normalizes and updates the yaml file' do + allow($stderr).to receive(:print) + expect { described_class.new(yaml.path).call } + .to change { File.read(yaml.path) } + .from(File.read("#{path}#{file}")) + .to(File.read("#{path}valid_normalized.yml")) + end + + it 'prints out a success message with relative file path' do + relative_path = Pathname.new(yaml.path).realpath.relative_path_from(Pathname.new(Dir.pwd)) + + expect { described_class.new(yaml.path).call } + .to output("[NORMALIZED] #{relative_path}\n").to_stderr + end + end + + context 'when YAML file contains multiple valid documents and byte order mark' do + let(:file) { 'valid_bom.yml' } + + it 'normalizes the yaml file' do + relative_path = Pathname.new(yaml.path).realpath.relative_path_from(Pathname.new(Dir.pwd)) + + expect { described_class.new(yaml.path).call }.to output("[NORMALIZED] #{relative_path}\n").to_stderr + end + end + + context 'when at least one normalization is not stable' do + let(:file) { 'valid2.yml' } + + it 'prints out an error to STDERR' do + normalize = described_class.new(yaml.path) + + allow(normalize).to receive(:convert).with(read(path + file)).and_call_original + allow(normalize).to receive(:convert).with(read("#{path}valid2_normalized.yml")).and_return(defect) + + relative_path = Pathname.new(yaml.path).realpath.relative_path_from(Pathname.new(Dir.pwd)) + expect { normalize.call }.to output("[ERROR] Could not normalize #{relative_path}\n").to_stderr + end + + def read(file) + File.read(file) + end + + def defect + [ + { error: nil }.extend(YamlNormalizer::Ext::Namespaced), + { 'that' => 'one, too' }.extend(YamlNormalizer::Ext::Namespaced) + ] + end + end + end +end diff --git a/spec/yaml_normalizer_spec.rb b/spec/yaml_normalizer_spec.rb index 49ce4db..7e0eaf1 100644 --- a/spec/yaml_normalizer_spec.rb +++ b/spec/yaml_normalizer_spec.rb @@ -4,6 +4,6 @@ RSpec.describe YamlNormalizer do it 'has a version number' do - expect(YamlNormalizer::VERSION).not_to be nil + expect(YamlNormalizer::VERSION).not_to be_nil end end diff --git a/yaml_normalizer.gemspec b/yaml_normalizer.gemspec index 5582c13..30f5914 100644 --- a/yaml_normalizer.gemspec +++ b/yaml_normalizer.gemspec @@ -29,23 +29,5 @@ Gem::Specification.new do |spec| spec.add_dependency 'psych', '~> 5.0' spec.add_dependency 'rake', '< 14.0' - spec.add_development_dependency 'awesome_print' - spec.add_development_dependency 'bundler' - spec.add_development_dependency 'flog' - spec.add_development_dependency 'github-markup' - spec.add_development_dependency 'guard' - spec.add_development_dependency 'guard-rspec' - spec.add_development_dependency 'guard-rubocop' - spec.add_development_dependency 'inch' - spec.add_development_dependency 'mutant-rspec' - spec.add_development_dependency 'pry' - spec.add_development_dependency 'pry-byebug' - spec.add_development_dependency 'pry-doc' - spec.add_development_dependency 'rb-readline' - spec.add_development_dependency 'redcarpet' - spec.add_development_dependency 'rspec' - spec.add_development_dependency 'rubocop' - spec.add_development_dependency 'rubocop-performance' - spec.add_development_dependency 'simplecov' - spec.add_development_dependency 'yard' + spec.metadata['rubygems_mfa_required'] = 'true' end