From 53303305a4ca719fe7750073976298a630f709cb Mon Sep 17 00:00:00 2001 From: Semyon Pupkov Date: Mon, 10 Apr 2017 18:50:41 +0500 Subject: [PATCH] Migrate from Minitest to RSpec (#200) * Migrate from Minitest to RSpec Closes https://github.com/hanami/utils/issues/198 * run tests with script/ci * Fix rubocop errors * run isolation tests without bundler load * Add isolation spec helper --- .rspec | 2 + .rubocop.yml | 1 + .travis.yml | 2 +- Rakefile | 18 +- hanami-utils.gemspec | 2 +- lib/hanami/logger.rb | 2 +- script/ci | 21 +- spec/hanami/interactor_spec.rb | 447 +++ spec/hanami/logger_spec.rb | 438 +++ spec/hanami/utils/basic_object_spec.rb | 42 + spec/hanami/utils/blank_spec.rb | 42 + spec/hanami/utils/callbacks_spec.rb | 333 +++ spec/hanami/utils/class_attribute_spec.rb | 147 + spec/hanami/utils/class_spec.rb | 127 + spec/hanami/utils/deprecation_spec.rb | 39 + spec/hanami/utils/duplicable_spec.rb | 99 + spec/hanami/utils/escape_spec.rb | 378 +++ spec/hanami/utils/file_list_spec.rb | 14 + spec/hanami/utils/hash_spec.rb | 487 +++ spec/hanami/utils/inflector_spec.rb | 140 + spec/hanami/utils/io_spec.rb | 17 + spec/hanami/utils/kernel_spec.rb | 2625 +++++++++++++++++ spec/hanami/utils/load_paths_spec.rb | 216 ++ spec/hanami/utils/path_prefix_spec.rb | 188 ++ spec/hanami/utils/string_spec.rb | 423 +++ spec/hanami/utils/utils_spec.rb | 21 + spec/hanami/utils/version_spec.rb | 5 + spec/isolation/.rspec | 1 + spec/isolation/json/json_spec.rb | 29 + spec/isolation/json/multi_json_spec.rb | 40 + spec/isolation/reload_spec.rb | 44 + .../require/with_absolute_path_spec.rb | 17 + .../require/with_file_separator_spec.rb | 22 + .../require/with_recursive_pattern_spec.rb | 17 + .../require/with_relative_path_spec.rb | 17 + spec/spec_helper.rb | 9 + spec/support/fixtures/file_list/a.rb | 2 + spec/support/fixtures/file_list/aa.rb | 2 + spec/support/fixtures/file_list/ab.rb | 2 + spec/support/fixtures/file_list/nested/c.rb | 2 + spec/support/fixtures/fixtures.rb | 628 ++++ spec/support/isolation_spec_helper.rb | 17 + spec/support/rspec.rb | 25 + spec/support/stdout.rb | 19 + test/class_attribute_test.rb | 22 +- test/interactor_test.rb | 2 +- test/isolation/reload_test.rb | 24 +- 47 files changed, 7178 insertions(+), 39 deletions(-) create mode 100644 .rspec create mode 100644 spec/hanami/interactor_spec.rb create mode 100644 spec/hanami/logger_spec.rb create mode 100644 spec/hanami/utils/basic_object_spec.rb create mode 100644 spec/hanami/utils/blank_spec.rb create mode 100644 spec/hanami/utils/callbacks_spec.rb create mode 100644 spec/hanami/utils/class_attribute_spec.rb create mode 100644 spec/hanami/utils/class_spec.rb create mode 100644 spec/hanami/utils/deprecation_spec.rb create mode 100644 spec/hanami/utils/duplicable_spec.rb create mode 100644 spec/hanami/utils/escape_spec.rb create mode 100644 spec/hanami/utils/file_list_spec.rb create mode 100644 spec/hanami/utils/hash_spec.rb create mode 100644 spec/hanami/utils/inflector_spec.rb create mode 100644 spec/hanami/utils/io_spec.rb create mode 100644 spec/hanami/utils/kernel_spec.rb create mode 100644 spec/hanami/utils/load_paths_spec.rb create mode 100644 spec/hanami/utils/path_prefix_spec.rb create mode 100644 spec/hanami/utils/string_spec.rb create mode 100644 spec/hanami/utils/utils_spec.rb create mode 100644 spec/hanami/utils/version_spec.rb create mode 100644 spec/isolation/.rspec create mode 100644 spec/isolation/json/json_spec.rb create mode 100644 spec/isolation/json/multi_json_spec.rb create mode 100644 spec/isolation/reload_spec.rb create mode 100644 spec/isolation/require/with_absolute_path_spec.rb create mode 100644 spec/isolation/require/with_file_separator_spec.rb create mode 100644 spec/isolation/require/with_recursive_pattern_spec.rb create mode 100644 spec/isolation/require/with_relative_path_spec.rb create mode 100644 spec/spec_helper.rb create mode 100644 spec/support/fixtures/file_list/a.rb create mode 100644 spec/support/fixtures/file_list/aa.rb create mode 100644 spec/support/fixtures/file_list/ab.rb create mode 100644 spec/support/fixtures/file_list/nested/c.rb create mode 100644 spec/support/fixtures/fixtures.rb create mode 100644 spec/support/isolation_spec_helper.rb create mode 100644 spec/support/rspec.rb create mode 100644 spec/support/stdout.rb diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..83e16f8 --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--color +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml index 8df0961..9c73328 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -6,6 +6,7 @@ Metrics/BlockLength: Exclude: - 'test/**/*' - 'tmp/**/*' + - 'spec/**/*' Style/DoubleNegation: Enabled: false Style/SpecialGlobalVars: diff --git a/.travis.yml b/.travis.yml index 2bcd87c..f1ace64 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: ruby sudo: false cache: bundler -script: 'script/ci && bundle exec rubocop' +script: ./script/ci before_install: - rvm @global do gem uninstall bundler -a -x - rvm @global do gem install bundler -v 1.13.7 diff --git a/Rakefile b/Rakefile index 415a74f..6d1957c 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,8 @@ require 'rake' -require 'rake/testtask' require 'bundler/gem_tasks' +require 'rspec/core/rake_task' +require 'rake/testtask' Rake::TestTask.new do |t| t.test_files = Dir['test/**/*_test.rb'].reject do |path| path.include?('isolation') @@ -10,11 +11,18 @@ Rake::TestTask.new do |t| t.libs.push 'test' end -namespace :test do +namespace :spec do + RSpec::Core::RakeTask.new(:unit) do |task| + file_list = FileList['spec/**/*_spec.rb'] + file_list = file_list.exclude("spec/{integration,isolation}/**/*_spec.rb") + + task.pattern = file_list + end + task :coverage do - ENV['COVERALL'] = 'true' - Rake::Task['test'].invoke + ENV['COVERAGE'] = 'true' + Rake::Task['spec:unit'].invoke end end -task default: :test +task default: 'spec:unit' diff --git a/hanami-utils.gemspec b/hanami-utils.gemspec index 00f8e60..d660e7c 100644 --- a/hanami-utils.gemspec +++ b/hanami-utils.gemspec @@ -1,4 +1,3 @@ -# coding: utf-8 lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'hanami/utils/version' @@ -21,4 +20,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'bundler', '~> 1.6' spec.add_development_dependency 'rake', '~> 11' + spec.add_development_dependency 'rspec', '~> 3.5' end diff --git a/lib/hanami/logger.rb b/lib/hanami/logger.rb index 2beb5d4..b4a6924 100644 --- a/lib/hanami/logger.rb +++ b/lib/hanami/logger.rb @@ -110,7 +110,7 @@ module Hanami # @since 1.0.0.beta1 # @api private - RESERVED_KEYS = [:app, :severity, :time].freeze + RESERVED_KEYS = %i(app severity time).freeze include Utils::ClassAttribute diff --git a/script/ci b/script/ci index 964e936..97e3431 100755 --- a/script/ci +++ b/script/ci @@ -7,16 +7,16 @@ run_code_quality_checks() { } run_unit_tests() { - bundle exec rake test:coverage + bundle exec rake spec:coverage } -run_integration_tests() { +run_isolation_tests() { local pwd=$PWD - local root="$pwd/test/isolation" + local root="$pwd/spec/isolation" - for test in $(find $root -name '*_test.rb') + for test in $(find $root -name '*_spec.rb') do - run_test $test + run_isolation_test $test if [ $? -ne 0 ]; then local exit_code=$? @@ -26,17 +26,24 @@ run_integration_tests() { done } +run_isolation_test() { + local test=$1 + + printf "\n\n\nRunning: $test\n" + ruby $test --options spec/isolation/.rspec +} + run_test() { local test=$1 printf "\n\n\nRunning: $test\n" - ruby -Itest $test + COVERAGE=true bundle exec rspec $test } main() { run_code_quality_checks && run_unit_tests && - run_integration_tests + run_isolation_tests } main diff --git a/spec/hanami/interactor_spec.rb b/spec/hanami/interactor_spec.rb new file mode 100644 index 0000000..b3fd4c2 --- /dev/null +++ b/spec/hanami/interactor_spec.rb @@ -0,0 +1,447 @@ +require 'hanami/interactor' + +class InteractorWithoutInitialize + include Hanami::Interactor + + def call + end +end + +class InteractorWithoutCall + include Hanami::Interactor +end + +class User + def initialize(attributes = {}) + @attributes = attributes + end + + def name + @attributes.fetch(:name, nil) + end + + def name=(value) + @attributes[:name] = value + end + + def persist! + raise if name.nil? + end + + def to_hash + { name: name } + end +end + +class Signup + include Hanami::Interactor + expose :user, :params + + def initialize(params) + @params = params + @user = User.new(params) + @__foo = 23 + end + + def call + @user.persist! + rescue + fail! + end + + private + + def valid? + !@params[:force_failure] + end +end + +class ErrorInteractor + include Hanami::Interactor + expose :operations + + def initialize + @operations = [] + end + + def call + prepare! + persist! + log! + end + + private + + def prepare! + @operations << __method__ + error 'There was an error while preparing data.' + end + + def persist! + @operations << __method__ + error 'There was an error while persisting data.' + end + + def log! + @operations << __method__ + end +end + +class ErrorBangInteractor + include Hanami::Interactor + expose :operations + + def initialize + @operations = [] + end + + def call + persist! + sync! + end + + private + + def persist! + @operations << __method__ + error! 'There was an error while persisting data.' + end + + def sync! + @operations << __method__ + error 'There was an error while syncing data.' + end +end + +class PublishVideo + include Hanami::Interactor + + def call + end + + def valid? + owns? + end + + private + + def owns? + # fake failed ownership check + 1 == 0 || + error("You're not owner of this video") + end +end + +class CreateUser + include Hanami::Interactor + expose :user + + def initialize(params) + @user = User.new(params) + end + + def call + persist + end + + private + + def persist + @user.persist! + end +end + +class UpdateUser < CreateUser + def initialize(_user, params) + super(params) + @user.name = params.fetch(:name) + end +end + +RSpec.describe Hanami::Interactor do + describe '#initialize' do + it "works when it isn't overridden" do + InteractorWithoutInitialize.new + end + + it 'allows to override it' do + Signup.new({}) + end + end + + describe '#call' do + it 'returns a result' do + result = Signup.new(name: 'Luca').call + expect(result.class).to eq Hanami::Interactor::Result + end + + it 'is successful by default' do + result = Signup.new(name: 'Luca').call + expect(result).to be_successful + end + + it 'returns the payload' do + result = Signup.new(name: 'Luca').call + + expect(result.user.name).to eq 'Luca' + expect(result.params).to eq(name: 'Luca') + end + + it "doesn't include private ivars" do + result = Signup.new(name: 'Luca').call + + expect { result.__foo }.to raise_error NoMethodError + end + + it 'exposes a convenient API for handling failures' do + result = Signup.new({}).call + expect(result).to be_failure + end + + it "doesn't invoke it if the preconditions are failing" do + result = Signup.new(force_failure: true).call + expect(result).to be_failure + end + + it "raises error when #call isn't implemented" do + expect { InteractorWithoutCall.new.call }.to raise_error NoMethodError + end + + describe 'inheritance' do + it 'is successful for super class' do + result = CreateUser.new(name: 'L').call + + expect(result).to be_successful + expect(result.user.name).to eq 'L' + end + + it 'is successful for sub class' do + user = User.new(name: 'L') + result = UpdateUser.new(user, name: 'MG').call + + expect(result).to be_successful + expect(result.user.name).to eq 'MG' + end + end + end + + describe '#error' do + it "isn't successful" do + result = ErrorInteractor.new.call + expect(result).to be_failure + end + + it 'accumulates errors' do + result = ErrorInteractor.new.call + expect(result.errors).to eq [ + 'There was an error while preparing data.', + 'There was an error while persisting data.' + ] + end + + it "doesn't interrupt the flow" do + result = ErrorInteractor.new.call + expect(result.operations).to eq %i(prepare! persist! log!) + end + + # See https://github.com/hanami/utils/issues/69 + it 'returns false as control flow for caller' do + interactor = PublishVideo.new + expect(interactor).not_to be_valid + end + end + + describe '#error!' do + it "isn't successful" do + result = ErrorBangInteractor.new.call + expect(result).to be_failure + end + + it 'stops at the first error' do + result = ErrorBangInteractor.new.call + expect(result.errors).to eq [ + 'There was an error while persisting data.' + ] + end + + it 'interrupts the flow' do + result = ErrorBangInteractor.new.call + expect(result.operations).to eq [:persist!] + end + end +end + +RSpec.describe Hanami::Interactor::Result do + describe '#initialize' do + it 'allows to skip payload' do + Hanami::Interactor::Result.new + end + + it 'accepts a payload' do + result = Hanami::Interactor::Result.new(foo: 'bar') + expect(result.foo).to eq 'bar' + end + end + + describe '#successful?' do + it 'is successful by default' do + result = Hanami::Interactor::Result.new + expect(result).to be_successful + end + + describe 'when it has errors' do + it "isn't successful" do + result = Hanami::Interactor::Result.new + result.add_error 'There was a problem' + expect(result).to be_failure + end + end + end + + describe '#fail!' do + it 'causes a failure' do + result = Hanami::Interactor::Result.new + result.fail! + + expect(result).to be_failure + end + end + + describe '#prepare!' do + it 'merges the current payload' do + result = Hanami::Interactor::Result.new(foo: 'bar') + result.prepare!(foo: 23) + + expect(result.foo).to eq 23 + end + + it 'returns self' do + result = Hanami::Interactor::Result.new(foo: 'bar') + returning = result.prepare!(foo: 23) + + expect(returning).to eq result + end + end + + describe '#errors' do + it 'empty by default' do + result = Hanami::Interactor::Result.new + expect(result.errors).to be_empty + end + + it 'returns all the errors' do + result = Hanami::Interactor::Result.new + result.add_error ['Error 1', 'Error 2'] + + expect(result.errors).to eq ['Error 1', 'Error 2'] + end + + it 'prevents information escape' do + result = Hanami::Interactor::Result.new + result.add_error ['Error 1', 'Error 2'] + + result.errors.clear + + expect(result.errors).to eq ['Error 1', 'Error 2'] + end + end + + describe '#error' do + it 'nil by default' do + result = Hanami::Interactor::Result.new + expect(result.error).to be_nil + end + + it 'returns only the first error' do + result = Hanami::Interactor::Result.new + result.add_error ['Error 1', 'Error 2'] + + expect(result.error).to eq 'Error 1' + end + end + + describe '#respond_to?' do + it 'returns true for concrete methods' do + result = Hanami::Interactor::Result.new + + expect(result).to respond_to(:successful?) + expect(result).to respond_to('successful?') + + expect(result).to respond_to(:failure?) + expect(result).to respond_to('failure?') + end + + it 'returns true for methods derived from payload' do + result = Hanami::Interactor::Result.new(foo: 1) + + expect(result).to respond_to(:foo) + expect(result).to respond_to('foo') + end + + it 'returns true for methods derived from merged payload' do + result = Hanami::Interactor::Result.new + result.prepare!(bar: 2) + + expect(result).to respond_to(:bar) + expect(result).to respond_to('bar') + end + end + + describe '#inspect' do + let(:result) { Hanami::Interactor::Result.new(id: 23, user: User.new) } + + it 'reports the class name and the object_id' do + expect(result.inspect).to match %(#23, :user=># 3 }) + expect(result.a).to eq(100 => 3) + end + + it 'returns all the values after a merge' do + result = Hanami::Interactor::Result.new(a: 1, b: 2) + result.prepare!(a: 23, c: 3) + + expect(result.a).to eq 23 + expect(result.b).to eq 2 + expect(result.c).to eq 3 + end + + it "doesn't ignore forwarded messages" do + result = Hanami::Interactor::Result.new(params: { name: 'Luca' }) + expect(result.params[:name]).to eq 'Luca' + end + + it 'raises an error when unknown message is passed' do + result = Hanami::Interactor::Result.new + expect { result.unknown }.to raise_error NoMethodError + end + + it 'raises an error when unknown message is passed with args' do + result = Hanami::Interactor::Result.new + expect { result.unknown(:foo) }.to raise_error NoMethodError + end + end +end diff --git a/spec/hanami/logger_spec.rb b/spec/hanami/logger_spec.rb new file mode 100644 index 0000000..4c9c7fc --- /dev/null +++ b/spec/hanami/logger_spec.rb @@ -0,0 +1,438 @@ +require 'hanami/logger' +require 'rbconfig' + +RSpec.describe Hanami::Logger do + before do + # clear defined class + Object.send(:remove_const, :TestLogger) if Object.constants.include?(:TestLogger) + + allow(Time).to receive(:now).and_return(Time.parse("2017-01-15 16:00:23 +0100")) + end + + it 'like std logger, sets log level to info by default' do + class TestLogger < Hanami::Logger; end + expect(TestLogger.new.info?).to eq true + end + + describe '#initialize' do + it 'uses STDOUT by default' do + output = + with_captured_stdout do + class TestLogger < Hanami::Logger; end + logger = TestLogger.new + logger.info('foo') + end + + expect(output).to match(/foo/) + end + + describe 'custom level option' do + it 'takes a integer' do + logger = Hanami::Logger.new(level: 3) + expect(logger.level).to eq Hanami::Logger::ERROR + end + + it 'takes a integer more than 5' do + logger = Hanami::Logger.new(level: 99) + expect(logger.level).to eq Hanami::Logger::DEBUG + end + + it 'takes a symbol' do + logger = Hanami::Logger.new(level: :error) + expect(logger.level).to eq Hanami::Logger::ERROR + end + + it 'takes a string' do + logger = Hanami::Logger.new(level: 'error') + expect(logger.level).to eq Hanami::Logger::ERROR + end + + it 'takes a string with strange value' do + logger = Hanami::Logger.new(level: 'strange') + expect(logger.level).to eq Hanami::Logger::DEBUG + end + + it 'takes a uppercased string' do + logger = Hanami::Logger.new(level: 'ERROR') + expect(logger.level).to eq Hanami::Logger::ERROR + end + + it 'takes a constant' do + logger = Hanami::Logger.new(level: Hanami::Logger::ERROR) + expect(logger.level).to eq Hanami::Logger::ERROR + end + + it 'contains debug level by default' do + logger = Hanami::Logger.new + expect(logger.level).to eq ::Logger::DEBUG + end + end + + describe 'custom stream' do + describe 'file system' do + before do + Pathname.new(stream).dirname.mkpath + end + + Hash[ + Pathname.new(Dir.pwd).join('tmp', 'logfile.log').to_s => 'absolute path (string)', + Pathname.new('tmp').join('logfile.log').to_s => 'relative path (string)', + Pathname.new(Dir.pwd).join('tmp', 'logfile.log') => 'absolute path (pathname)', + Pathname.new('tmp').join('logfile.log') => 'relative path (pathname)', + ].each do |dev, desc| + describe "when #{desc}" do + let(:stream) { dev } + + after do + File.delete(stream) + end + + describe 'and it does not exist' do + before do + File.delete(stream) if File.exist?(stream) + end + + it 'writes to file' do + logger = Hanami::Logger.new(stream: stream) + logger.info('newline') + + contents = File.read(stream) + expect(contents).to match(/newline/) + end + end + + describe 'and it already exists' do + before do + File.open(stream, File::WRONLY | File::TRUNC | File::CREAT, permissions) { |f| f.write('existing') } + end + + let(:permissions) { 0o664 } + + it 'appends to file' do + logger = Hanami::Logger.new(stream: stream) + logger.info('appended') + + contents = File.read(stream) + expect(contents).to match(/existing/) + expect(contents).to match(/appended/) + end + + it 'does not change permissions' do + logger = Hanami::Logger.new(stream: stream) + logger.info('appended') + end + end + end + end # end loop + + describe 'when file' do + let(:stream) { Pathname.new('tmp').join('logfile.log') } + let(:log_file) { File.new(stream, 'w+', permissions) } + let(:permissions) { 0o644 } + + before(:each) do + log_file.write('hello') + end + + describe 'and already written' do + it 'appends to file' do + logger = Hanami::Logger.new(stream: log_file) + logger.info('world') + + logger.close + + contents = File.read(log_file) + expect(contents).to match(/hello/) + expect(contents).to match(/world/) + end + + it 'does not change permissions' + # it 'does not change permissions' do + # logger = Hanami::Logger.new(stream: log_file) + # logger.info('appended') + # logger.close + + # stat = File.stat(log_file) + # mode = stat.mode.to_s(8) + + # require 'hanami/utils' + # if Hanami::Utils.jruby? + # expect(mode).to eq('100664') + # else + # expect(mode).to eq('100644') + # end + # end + end + end # end File + + describe 'when IO' do + let(:stream) { Pathname.new('tmp').join('logfile.log').to_s } + + it 'appends' do + fd = IO.sysopen(stream, 'w') + io = IO.new(fd, 'w') + + logger = Hanami::Logger.new(stream: io) + logger.info('in file') + logger.close + + contents = File.read(stream) + expect(contents).to match(/in file/) + end + end # end IO + end # end FileSystem + + describe 'when StringIO' do + let(:stream) { StringIO.new } + + it 'appends' do + logger = Hanami::Logger.new(stream: stream) + logger.info('in file') + + stream.rewind + expect(stream.read).to match(/in file/) + end + end # end StringIO + end # end #initialize + + describe '#close' do + it 'does not close STDOUT output for other code' do + logger = Hanami::Logger.new(stream: STDOUT) + logger.close + + expect { print 'in STDOUT' }.to output('in STDOUT').to_stdout + end + + it 'does not close $stdout output for other code' do + logger = Hanami::Logger.new(stream: $stdout) + logger.close + + expect { print 'in $stdout' }.to output('in $stdout').to_stdout + end + end + + describe '#level=' do + subject(:logger) { Hanami::Logger.new } + + it 'takes a integer' do + logger.level = 3 + + expect(logger.level).to eq Hanami::Logger::ERROR + end + + it 'takes a integer more than 5' do + logger.level = 99 + + expect(logger.level).to eq Hanami::Logger::DEBUG + end + + it 'takes a symbol' do + logger.level = :error + + expect(logger.level).to eq Hanami::Logger::ERROR + end + + it 'takes a string' do + logger.level = 'error' + + expect(logger.level).to eq Hanami::Logger::ERROR + end + + it 'takes a string with strange value' do + logger.level = 'strange' + + expect(logger.level).to eq Hanami::Logger::DEBUG + end + + it 'takes a uppercased string' do + logger.level = 'ERROR' + + expect(logger.level).to eq Hanami::Logger::ERROR + end + + it 'takes a constant' do + logger.level = Hanami::Logger::ERROR + + expect(logger.level).to eq Hanami::Logger::ERROR + end + end + + it 'has application_name when log' do + output = + with_captured_stdout do + module App; class TestLogger < Hanami::Logger; end; end + logger = App::TestLogger.new + logger.info('foo') + end + + expect(output).to match(/App/) + end + + it 'has default app tag when not in any namespace' do + class TestLogger < Hanami::Logger; end + expect(TestLogger.new.application_name).to eq 'hanami' + end + + it 'infers apptag from namespace' do + module App2 + class TestLogger < Hanami::Logger; end + class Bar + def hoge + TestLogger.new.application_name + end + end + end + + expect(App2::Bar.new.hoge).to eq 'App2' + end + + it 'uses custom application_name from override class' do + class TestLogger < Hanami::Logger + def application_name + 'bar' + end + end + + output = + with_captured_stdout do + TestLogger.new.info('') + end + + expect(output).to match(/bar/) + end + + describe 'with nil formatter' do + it 'falls back to Formatter' do + output = + with_captured_stdout do + class TestLogger < Hanami::Logger; end + TestLogger.new(formatter: nil).info('foo') + end + expect(output).to eq "[hanami] [INFO] [2017-01-15 16:00:23 +0100] foo\n" + end + end + + describe 'with JSON formatter' do + if Hanami::Utils.jruby? + it 'when passed as a symbol, it has JSON format for string messages' + else + it 'when passed as a symbol, it has JSON format for string messages' do + output = + with_captured_stdout do + class TestLogger < Hanami::Logger; end + TestLogger.new(formatter: :json).info('foo') + end + expect(output).to eq %({"app":"hanami","severity":"INFO","time":"2017-01-15T15:00:23Z","message":"foo"}\n) + end + end + + if Hanami::Utils.jruby? + it 'has JSON format for string messages' + else + it 'has JSON format for string messages' do + output = + with_captured_stdout do + class TestLogger < Hanami::Logger; end + TestLogger.new(formatter: Hanami::Logger::JSONFormatter.new).info('foo') + end + expect(output).to eq %({"app":"hanami","severity":"INFO","time":"2017-01-15T15:00:23Z","message":"foo"}\n) + end + end + + if Hanami::Utils.jruby? + it 'has JSON format for error messages' + else + it 'has JSON format for error messages' do + output = + with_captured_stdout do + class TestLogger < Hanami::Logger; end + TestLogger.new(formatter: Hanami::Logger::JSONFormatter.new).error(Exception.new('foo')) + end + expect(output).to eq %({"app":"hanami","severity":"ERROR","time":"2017-01-15T15:00:23Z","message":"foo","backtrace":[],"error":"Exception"}\n) + end + end + + if Hanami::Utils.jruby? + it 'has JSON format for hash messages' + else + it 'has JSON format for hash messages' do + output = + with_captured_stdout do + class TestLogger < Hanami::Logger; end + TestLogger.new(formatter: Hanami::Logger::JSONFormatter.new).info(foo: :bar) + end + expect(output).to eq %({"app":"hanami","severity":"INFO","time":"2017-01-15T15:00:23Z","foo":"bar"}\n) + end + end + + if Hanami::Utils.jruby? + it 'has JSON format for not string messages' + else + it 'has JSON format for not string messages' do + output = + with_captured_stdout do + class TestLogger < Hanami::Logger; end + TestLogger.new(formatter: Hanami::Logger::JSONFormatter.new).info(['foo']) + end + expect(output).to eq %({"app":"hanami","severity":"INFO","time":"2017-01-15T15:00:23Z","message":["foo"]}\n) + end + end + end + + describe 'with default formatter' do + it 'when passed as a symbol, it has key=value format for string messages' do + output = + with_captured_stdout do + class TestLogger < Hanami::Logger; end + TestLogger.new(formatter: :default).info('foo') + end + expect(output).to eq "[hanami] [INFO] [2017-01-15 16:00:23 +0100] foo\n" + end + + it 'has key=value format for string messages' do + output = + with_captured_stdout do + class TestLogger < Hanami::Logger; end + TestLogger.new.info('foo') + end + expect(output).to eq "[hanami] [INFO] [2017-01-15 16:00:23 +0100] foo\n" + end + + it 'has key=value format for error messages' do + exception = nil + output = with_captured_stdout do + class TestLogger < Hanami::Logger; end + begin + raise StandardError.new('foo') + rescue => e + exception = e + end + TestLogger.new.error(exception) + end + expectation = "[hanami] [ERROR] [2017-01-15 16:00:23 +0100] StandardError: foo\n" + exception.backtrace.each do |line| + expectation << "from #{line}\n" + end + expect(output).to eq expectation + end + + it 'has key=value format for hash messages' do + output = + with_captured_stdout do + class TestLogger < Hanami::Logger; end + TestLogger.new.info(foo: :bar) + end + expect(output).to eq "[hanami] [INFO] [2017-01-15 16:00:23 +0100] bar\n" + end + + it 'has key=value format for not string messages' do + output = + with_captured_stdout do + class TestLogger < Hanami::Logger; end + TestLogger.new.info(%(foo bar)) + end + expect(output).to eq "[hanami] [INFO] [2017-01-15 16:00:23 +0100] foo bar\n" + end + end + end +end diff --git a/spec/hanami/utils/basic_object_spec.rb b/spec/hanami/utils/basic_object_spec.rb new file mode 100644 index 0000000..cc35686 --- /dev/null +++ b/spec/hanami/utils/basic_object_spec.rb @@ -0,0 +1,42 @@ +require 'hanami/utils/basic_object' +require 'pp' + +class TestClass < Hanami::Utils::BasicObject +end + +RSpec.describe Hanami::Utils::BasicObject do + describe '#respond_to_missing?' do + it 'raises an exception if respond_to? method is not implemented' do + expect { TestClass.new.respond_to?(:no_existing_method) } + .to raise_error(NotImplementedError) + end + + it 'returns true given respond_to? method was implemented' do + TestCase = Class.new(TestClass) do + def respond_to?(_method_name, _include_all = false) + true + end + end + + expect(TestCase.new).to respond_to(:no_existing_method) + end + end + + describe '#class' do + it 'returns TestClass' do + expect(TestClass.new.class).to eq TestClass + end + end + + describe '#inspect' do + it 'returns the inspect message' do + inspect_msg = TestClass.new.inspect + expect(inspect_msg).to match(/\A#\z/) + end + end + + # See https://github.com/hanami/hanami/issues/629 + it 'is pretty printable' do + pp TestClass.new + end +end diff --git a/spec/hanami/utils/blank_spec.rb b/spec/hanami/utils/blank_spec.rb new file mode 100644 index 0000000..5b70ce0 --- /dev/null +++ b/spec/hanami/utils/blank_spec.rb @@ -0,0 +1,42 @@ +require 'hanami/utils/kernel' +require 'hanami/utils/string' +require 'hanami/utils/hash' +require 'hanami/utils/blank' + +RSpec.describe Hanami::Utils::Blank do + describe '.blank?' do + [nil, false, '', ' ', " \n\t \r ", ' ', "\u00a0", [], {}, Set.new, + Hanami::Utils::Kernel.Boolean(0), Hanami::Utils::String.new(''), + Hanami::Utils::Hash.new({})].each do |v| + it 'returns true' do + expect(Hanami::Utils::Blank.blank?(v)).to eq(true) + end + end + + [Object.new, true, 0, 1, 'a', :book, DateTime.now, Time.now, Date.new, [nil], { nil => 0 }, Set.new([1]), + Hanami::Utils::Kernel.Symbol(:hello), Hanami::Utils::String.new('foo'), + Hanami::Utils::Hash.new(foo: :bar)].each do |v| + it 'returns false' do + expect(Hanami::Utils::Blank.blank?(v)).to eq(false) + end + end + end + + describe '.filled?' do + [nil, false, '', ' ', " \n\t \r ", ' ', "\u00a0", [], {}, Set.new, + Hanami::Utils::Kernel.Boolean(0), Hanami::Utils::String.new(''), + Hanami::Utils::Hash.new({})].each do |v| + it 'returns false' do + expect(Hanami::Utils::Blank.filled?(v)).to eq(false) + end + end + + [Object.new, true, 0, 1, 'a', :book, DateTime.now, Time.now, Date.new, [nil], { nil => 0 }, Set.new([1]), + Hanami::Utils::Kernel.Symbol(:hello), Hanami::Utils::String.new('foo'), + Hanami::Utils::Hash.new(foo: :bar)].each do |v| + it 'returns true' do + expect(Hanami::Utils::Blank.filled?(v)).to eq(true) + end + end + end +end diff --git a/spec/hanami/utils/callbacks_spec.rb b/spec/hanami/utils/callbacks_spec.rb new file mode 100644 index 0000000..ba8ed9a --- /dev/null +++ b/spec/hanami/utils/callbacks_spec.rb @@ -0,0 +1,333 @@ +require 'hanami/utils/callbacks' + +Hanami::Utils::Callbacks::Chain.class_eval do + def size + @chain.size + end + + def first + @chain.first + end + + def last + @chain.last + end + + def each(&blk) + @chain.each(&blk) + end +end + +class Callable + def call + end +end + +class Action + attr_reader :logger + + def initialize + @logger = [] + end + + private + + def authenticate! + logger.push 'authenticate!' + end + + def set_article(params) # rubocop:disable Style/AccessorMethodName + logger.push "set_article: #{params[:id]}" + end +end + +RSpec.describe Hanami::Utils::Callbacks::Chain do + before do + @chain = Hanami::Utils::Callbacks::Chain.new + end + + describe '#append' do + it 'wraps the given callback with a callable object' do + @chain.append :symbolize! + + cb = @chain.last + expect(cb).to respond_to(:call) + end + + it 'appends the callbacks at the end of the chain' do + @chain.append(:foo) + + @chain.append(:bar) + expect(@chain.first.callback).to eq(:foo) + expect(@chain.last.callback).to eq(:bar) + end + + describe 'when a callable object is passed' do + before do + @chain.append callback + end + + let(:callback) { Callable.new } + + it 'includes the given callback' do + cb = @chain.last + expect(cb.callback).to eq(callback) + end + end + + describe 'when a Symbol is passed' do + before do + @chain.append callback + end + + let(:callback) { :upcase } + + it 'includes the given callback' do + cb = @chain.last + expect(cb.callback).to eq(callback) + end + + it 'guarantees unique entries' do + # append the callback again, see before block + @chain.append callback + expect(@chain.size).to eq(1) + end + end + + describe 'when a block is passed' do + before do + @chain.append(&callback) + end + + let(:callback) { proc {} } + + it 'includes the given callback' do + cb = @chain.last + expect(cb.callback).to eq(callback) + end + end + + describe 'when multiple callbacks are passed' do + before do + @chain.append(*callbacks) + end + + let(:callbacks) { [:upcase, Callable.new, proc {}] } + + it 'includes all the given callbacks' do + expect(@chain.size).to eq(callbacks.size) + end + + it 'all the included callbacks are callable' do + @chain.each do |callback| + expect(callback).to respond_to(:call) + end + end + end + end + + describe '#prepend' do + it 'wraps the given callback with a callable object' do + @chain.prepend :symbolize! + + cb = @chain.first + expect(cb).to respond_to(:call) + end + + it 'prepends the callbacks at the beginning of the chain' do + @chain.append(:foo) + + @chain.prepend(:bar) + expect(@chain.first.callback).to eq(:bar) + expect(@chain.last.callback).to eq(:foo) + end + + describe 'when a callable object is passed' do + before do + @chain.prepend callback + end + + let(:callback) { Callable.new } + + it 'includes the given callback' do + cb = @chain.first + expect(cb.callback).to eq(callback) + end + end + + describe 'when a Symbol is passed' do + before do + @chain.prepend callback + end + + let(:callback) { :upcase } + + it 'includes the given callback' do + cb = @chain.first + expect(cb.callback).to eq(callback) + end + + it 'guarantees unique entries' do + # append the callback again, see before block + @chain.prepend callback + expect(@chain.size).to eq(1) + end + end + + describe 'when a block is passed' do + before do + @chain.prepend(&callback) + end + + let(:callback) { proc {} } + + it 'includes the given callback' do + cb = @chain.first + expect(cb.callback).to eq callback + end + end + + describe 'when multiple callbacks are passed' do + before do + @chain.prepend(*callbacks) + end + + let(:callbacks) { [:upcase, Callable.new, proc {}] } + + it 'includes all the given callbacks' do + expect(@chain.size).to eq(callbacks.size) + end + + it 'all the included callbacks are callable' do + @chain.each do |callback| + expect(callback).to respond_to(:call) + end + end + end + end + + describe '#run' do + let(:action) { Action.new } + let(:params) { Hash[id: 23] } + + describe 'when symbols are passed' do + before do + @chain.append :authenticate!, :set_article + @chain.run action, params + end + + it 'executes the callbacks' do + authenticate = action.logger.shift + expect(authenticate).to eq 'authenticate!' + + set_article = action.logger.shift + expect(set_article).to eq "set_article: #{params[:id]}" + end + end + + describe 'when procs are passed' do + before do + @chain.append do + logger.push 'authenticate!' + end + + @chain.append do |params| + logger.push "set_article: #{params[:id]}" + end + + @chain.run action, params + end + + it 'executes the callbacks' do + authenticate = action.logger.shift + expect(authenticate).to eq 'authenticate!' + + set_article = action.logger.shift + expect(set_article).to eq "set_article: #{params[:id]}" + end + end + end + + describe '#freeze' do + before do + @chain.freeze + end + + it 'must be frozen' do + expect(@chain).to be_frozen + end + + it 'raises an error if try to add a callback when frozen' do + expect { @chain.append :authenticate! }.to raise_error RuntimeError + end + end +end + +RSpec.describe Hanami::Utils::Callbacks::Factory do + describe '.fabricate' do + before do + @callback = Hanami::Utils::Callbacks::Factory.fabricate(callback) + end + + describe 'when a callable is passed' do + let(:callback) { Callable.new } + + it 'fabricates a Callback' do + expect(@callback).to be_kind_of(Hanami::Utils::Callbacks::Callback) + end + + it 'wraps the given callback' do + expect(@callback.callback).to eq(callback) + end + end + + describe 'when a symbol is passed' do + let(:callback) { :symbolize! } + + it 'fabricates a MethodCallback' do + expect(@callback).to be_kind_of(Hanami::Utils::Callbacks::MethodCallback) + end + + it 'wraps the given callback' do + expect(@callback.callback).to eq(callback) + end + end + end +end + +RSpec.describe Hanami::Utils::Callbacks::Callback do + before do + @callback = Hanami::Utils::Callbacks::Callback.new(callback) + end + + let(:callback) { proc { |params| logger.push("set_article: #{params[:id]}") } } + + it 'executes self within the given context' do + context = Action.new + @callback.call(context, id: 23) + + invokation = context.logger.shift + expect(invokation).to eq('set_article: 23') + end +end + +RSpec.describe Hanami::Utils::Callbacks::MethodCallback do + before do + @callback = Hanami::Utils::Callbacks::MethodCallback.new(callback) + end + + let(:callback) { :set_article } + + it 'executes self within the given context' do + context = Action.new + @callback.call(context, id: 23) + + invokation = context.logger.shift + expect(invokation).to eq('set_article: 23') + end + + it 'implements #hash' do + cb = Hanami::Utils::Callbacks::MethodCallback.new(callback) + expect(cb.send(:hash)).to eq(@callback.send(:hash)) + end +end diff --git a/spec/hanami/utils/class_attribute_spec.rb b/spec/hanami/utils/class_attribute_spec.rb new file mode 100644 index 0000000..6374e3b --- /dev/null +++ b/spec/hanami/utils/class_attribute_spec.rb @@ -0,0 +1,147 @@ +require 'hanami/utils/class_attribute' + +RSpec.describe Hanami::Utils::ClassAttribute do + before do + class ClassAttributeTest + include Hanami::Utils::ClassAttribute + class_attribute :callbacks, :functions, :values + self.callbacks = [:a] + self.values = [1] + end + + class SubclassAttributeTest < ClassAttributeTest + class_attribute :subattribute + self.functions = %i(x y) + self.subattribute = 42 + end + + class SubSubclassAttributeTest < SubclassAttributeTest + end + + class Vehicle + include Hanami::Utils::ClassAttribute + class_attribute :engines, :wheels + + self.engines = 0 + self.wheels = 0 + end + + class Car < Vehicle + self.engines = 1 + self.wheels = 4 + end + + class Airplane < Vehicle + self.engines = 4 + self.wheels = 16 + end + + class SmallAirplane < Airplane + self.engines = 2 + self.wheels = 8 + end + end + + after do + %i(ClassAttributeTest + SubclassAttributeTest + SubSubclassAttributeTest + Vehicle + Car + Airplane + SmallAirplane).each do |const| + Object.send :remove_const, const + end + end + + it 'sets the given value' do + expect(ClassAttributeTest.callbacks).to eq([:a]) + end + + describe 'inheritance' do + around do |example| + @debug = $DEBUG + $DEBUG = true + + example.run + + $DEBUG = @debug + end + + it 'the value it is inherited by subclasses' do + expect(SubclassAttributeTest.callbacks).to eq([:a]) + end + + it 'if the superclass value changes it does not affects subclasses' do + ClassAttributeTest.functions = [:y] + expect(SubclassAttributeTest.functions).to eq(%i(x y)) + end + + it 'if the subclass value changes it does not affects superclass' do + SubclassAttributeTest.values = [3, 2] + expect(ClassAttributeTest.values).to eq([1]) + end + + describe 'when the subclass is defined in a different namespace' do + before do + module Lts + module Routing + class Resource + class Action + include Hanami::Utils::ClassAttribute + class_attribute :verb + end + + class New < Action + self.verb = :get + end + end + + class Resources < Resource + class New < Resource::New + end + end + end + end + end + + it 'refers to the superclass value' do + expect(Lts::Routing::Resources::New.verb).to eq :get + end + end + + # it 'if the subclass value changes it affects subclasses' do + # values = [3,2] + # SubclassAttributeTest.values = values + # expect(SubclassAttributeTest.values).to eq(values) + # expect(SubSubclassAttributeTest.values).to eq(values) + # end + + it 'if the subclass defines an attribute it should not be available for the superclass' do + $DEBUG = @debug + expect { ClassAttributeTest.subattribute }.to raise_error(NoMethodError) + end + + it 'if the subclass defines an attribute it should be available for its subclasses' do + expect(SubSubclassAttributeTest.subattribute).to eq 42 + end + + it 'preserves values within the inheritance chain' do + expect(Vehicle.engines).to eq 0 + expect(Vehicle.wheels).to eq 0 + + expect(Car.engines).to eq 1 + expect(Car.wheels).to eq 4 + + expect(Airplane.engines).to eq 4 + expect(Airplane.wheels).to eq 16 + + expect(SmallAirplane.engines).to eq 2 + expect(SmallAirplane.wheels).to eq 8 + end + + it "doesn't print warnings when it gets inherited" do + expect { Class.new(Vehicle) }.not_to output.to_stdout + end + end +end diff --git a/spec/hanami/utils/class_spec.rb b/spec/hanami/utils/class_spec.rb new file mode 100644 index 0000000..0edb445 --- /dev/null +++ b/spec/hanami/utils/class_spec.rb @@ -0,0 +1,127 @@ +require 'hanami/utils/class' + +RSpec.describe Hanami::Utils::Class do + before do + class Bar + def level + 'top' + end + end + + class Foo + class Bar + def level + 'nested' + end + end + end + + module App + module Layer + class Step + end + end + + module Service + class Point + end + end + + class ServicePoint + end + end + end + + describe '.load!' do + it 'loads the class from the given static string' do + expect(Hanami::Utils::Class.load!('App::Layer::Step')).to eq(App::Layer::Step) + end + + it 'loads the class from the given static string and namespace' do + expect(Hanami::Utils::Class.load!('Step', App::Layer)).to eq(App::Layer::Step) + end + + it 'loads the class from the given class name' do + expect(Hanami::Utils::Class.load!(App::Layer::Step)).to eq(App::Layer::Step) + end + + it 'raises an error in case of missing class' do + expect { Hanami::Utils::Class.load!('Missing') }.to raise_error(NameError) + end + end + + describe '.load' do + it 'loads the class from the given static string' do + expect(Hanami::Utils::Class.load('App::Layer::Step')).to eq(App::Layer::Step) + end + + it 'loads the class from the given static string and namespace' do + expect(Hanami::Utils::Class.load('Step', App::Layer)).to eq(App::Layer::Step) + end + + it 'loads the class from the given class name' do + expect(Hanami::Utils::Class.load(App::Layer::Step)).to eq(App::Layer::Step) + end + + it 'returns nil in case of missing class' do + expect(Hanami::Utils::Class.load('Missing')).to eq(nil) + end + end + + describe '.load_from_pattern!' do + it 'loads the class within the given namespace' do + klass = Hanami::Utils::Class.load_from_pattern!('(Hanami|Foo)::Bar') + expect(klass.new.level).to eq 'nested' + end + + it 'loads the class within the given namespace, when first namespace does not exist' do + klass = Hanami::Utils::Class.load_from_pattern!('(NotExisting|Foo)::Bar') + expect(klass.new.level).to eq 'nested' + end + + it 'loads the class within the given namespace when first namespace in pattern is correct one' do + klass = Hanami::Utils::Class.load_from_pattern!('(Foo|Hanami)::Bar') + expect(klass.new.level).to eq 'nested' + end + + it 'loads the class from the given static string' do + expect(Hanami::Utils::Class.load_from_pattern!('App::Layer::Step')).to eq(App::Layer::Step) + end + + it 'raises error for missing constant' do + expect { Hanami::Utils::Class.load_from_pattern!('MissingConstant') } + .to raise_error(NameError, 'uninitialized constant MissingConstant') + end + + it 'raises error for missing constant with multiple alternatives' do + expect { Hanami::Utils::Class.load_from_pattern!('Missing(Constant|Class)') } + .to raise_error(NameError, 'uninitialized constant Missing(Constant|Class)') + end + + it 'raises error with full constant name' do + expect { Hanami::Utils::Class.load_from_pattern!('Step', App) } + .to raise_error(NameError, 'uninitialized constant App::Step') + end + + it 'raises error with full constant name and multiple alternatives' do + expect { Hanami::Utils::Class.load_from_pattern!('(Step|Point)', App) } + .to raise_error(NameError, 'uninitialized constant App::(Step|Point)') + end + + it 'loads the class from given string, by interpolating tokens' do + expect(Hanami::Utils::Class.load_from_pattern!('App::Service(::Point|Point)')).to eq(App::Service::Point) + end + + it 'loads the class from given string, by interpolating string tokens and respecting their order' do + expect(Hanami::Utils::Class.load_from_pattern!('App::Service(Point|::Point)')).to eq(App::ServicePoint) + end + + it 'loads the class from given string, by interpolating tokens and not stopping after first fail' do + expect(Hanami::Utils::Class.load_from_pattern!('App::(Layer|Layer::)Step')).to eq(App::Layer::Step) + end + + it 'loads class from given string and namespace' do + expect(Hanami::Utils::Class.load_from_pattern!('(Layer|Layer::)Step', App)).to eq(App::Layer::Step) + end + end +end diff --git a/spec/hanami/utils/deprecation_spec.rb b/spec/hanami/utils/deprecation_spec.rb new file mode 100644 index 0000000..68d9162 --- /dev/null +++ b/spec/hanami/utils/deprecation_spec.rb @@ -0,0 +1,39 @@ +require 'hanami/utils/deprecation' + +class DeprecationTest + def old_method + Hanami::Utils::Deprecation.new('old_method is deprecated, please use new_method') + new_method + end + + def new_method + end +end + +class DeprecationWrapperTest + def initialize + @engine = DeprecationTest.new + end + + def run + @engine.old_method + end +end + +RSpec.describe Hanami::Utils::Deprecation do + it 'prints a deprecation warning for direct call' do + stack = if Hanami::Utils.jruby? + "#{__FILE__}:31:in `block in (root)'" + else + "#{__FILE__}:31:in `block (3 levels) in '" + end + + expect { DeprecationTest.new.old_method } + .to output(include("old_method is deprecated, please use new_method - called from: #{stack}.")).to_stderr + end + + it 'prints a deprecation warning for nested call' do + expect { DeprecationWrapperTest.new.run } + .to output(include("old_method is deprecated, please use new_method - called from: #{__FILE__}:19:in `run'.")).to_stderr + end +end diff --git a/spec/hanami/utils/duplicable_spec.rb b/spec/hanami/utils/duplicable_spec.rb new file mode 100644 index 0000000..662aad8 --- /dev/null +++ b/spec/hanami/utils/duplicable_spec.rb @@ -0,0 +1,99 @@ +require 'set' +require 'bigdecimal' +require 'hanami/utils/duplicable' + +RSpec.describe Hanami::Utils::Duplicable do + describe '#dup' do + describe 'non duplicable types' do + before do + @debug = $DEBUG + $DEBUG = true + end + + after do + $DEBUG = @debug + end + + it "doesn't dup nil" do + assert_same_duped_object nil + end + + it "doesn't dup false" do + assert_same_duped_object false + end + + it "doesn't dup true" do + assert_same_duped_object true + end + + it "doesn't dup symbol" do + assert_same_duped_object :hanami + end + + it "doesn't dup integer" do + assert_same_duped_object 23 + end + + it "doesn't dup float" do + assert_same_duped_object 3.14 + end + + it "doesn't dup bigdecimal" do + assert_same_duped_object BigDecimal.new(42) + end + + it "doesn't dup bignum" do + assert_same_duped_object 70_207_105_185_500**64 + end + end + + describe 'duplicable types' do + it 'duplicates array' do + assert_different_duped_object [2, [3, 'L']] + end + + it 'duplicates set' do + assert_different_duped_object Set.new(['L']) + end + + it 'duplicates hash' do + assert_different_duped_object Hash['L' => 23] + end + + it 'duplicates string' do + assert_different_duped_object 'Hanami' + end + + it 'duplicates date' do + assert_different_duped_object Date.today + end + + it 'duplicates time' do + assert_different_duped_object Time.now + end + + it 'duplicates datetime' do + assert_different_duped_object DateTime.now + end + end + + private + + def assert_same_duped_object(object) + actual = nil + + expect { actual = Hanami::Utils::Duplicable.dup(object) } + .to output(be_empty).to_stderr + + expect(actual).to eq object + expect(actual.object_id).to eq object.object_id + end + + def assert_different_duped_object(object) + actual = Hanami::Utils::Duplicable.dup(object) + + expect(actual).to eq object + expect(actual.object_id).not_to eq object.object_id + end + end +end diff --git a/spec/hanami/utils/escape_spec.rb b/spec/hanami/utils/escape_spec.rb new file mode 100644 index 0000000..99f3d07 --- /dev/null +++ b/spec/hanami/utils/escape_spec.rb @@ -0,0 +1,378 @@ +require 'hanami/utils' +require 'hanami/utils/escape' + +RSpec.describe Hanami::Utils::Escape do + let(:mod) { Hanami::Utils::Escape } + + TEST_ENCODINGS = Encoding.name_list.each_with_object(['UTF-8']) do |encoding, result| + test_string = '') do + result = mod.html('"">'.encode(encoding)) + expect(result).to eq '""><script>xss(5)</script>' + end + + it %(escapes '>') do + result = mod.html('>'.encode(encoding)) + expect(result).to eq '><script>xss(6)</script>' + end + + it %(escapes '# onmouseover="xss(7)" ') do + result = mod.html('# onmouseover="xss(7)" '.encode(encoding)) + expect(result).to eq '# onmouseover="xss(7)" ' + end + + it %(escapes '/" onerror="xss(9)">') do + result = mod.html('/" onerror="xss(9)">'.encode(encoding)) + expect(result).to eq '/" onerror="xss(9)">' + end + + it %(escapes '/ onerror="xss(10)"') do + result = mod.html('/ onerror="xss(10)"'.encode(encoding)) + expect(result).to eq '/ onerror="xss(10)"' + end + + it %(escapes '<') do + result = mod.html('<'.encode(encoding)) + expect(result).to eq '<<script>xss(14);//<</script>' + end + end + end + + it 'escapes word with different encoding' do + skip 'There is no ASCII-8BIT encoding' unless Encoding.name_list.include?('ASCII-8BIT') + + # rubocop:disable Style/AsciiComments + # 'тест' means test in russian + string = 'тест'.force_encoding('ASCII-8BIT') + encoding = string.encoding + + result = mod.html(string) + expect(result).to eq 'тест' + expect(result.encoding).to eq Encoding::UTF_8 + + expect(string.encoding).to eq encoding + end + end + + describe '.html_attribute' do + TEST_ENCODINGS.each do |encoding| + describe encoding.to_s do + it "doesn't escape safe string" do + input = Hanami::Utils::Escape::SafeString.new('&') + result = mod.html_attribute(input.encode(encoding)) + expect(result).to eq '&' + end + + it 'escapes nil' do + result = mod.html_attribute(nil) + expect(result).to eq '' + end + + it "escapes 'test'" do + result = mod.html_attribute('test'.encode(encoding)) + expect(result).to eq 'test' + end + + it "escapes '&'" do + result = mod.html_attribute('&'.encode(encoding)) + expect(result).to eq '&' + end + + it "escapes '<'" do + result = mod.html_attribute('<'.encode(encoding)) + expect(result).to eq '<' + end + + it "escapes '>'" do + result = mod.html_attribute('>'.encode(encoding)) + expect(result).to eq '>' + end + + it %(escapes '"') do + result = mod.html_attribute('"'.encode(encoding)) + expect(result).to eq '"' + end + + it %(escapes "'") do + result = mod.html_attribute("'".encode(encoding)) + expect(result).to eq ''' + end + + it "escapes '/'" do + result = mod.html_attribute('/'.encode(encoding)) + expect(result).to eq '/' + end + + it "escapes '') do + result = mod.html_attribute('"">'.encode(encoding)) + expect(result).to eq '""><script>xss(5)</script>' + end + + it %(escapes '>') do + result = mod.html_attribute('>'.encode(encoding)) + expect(result).to eq '><script>xss(6)</script>' + end + + it %(escapes '# onmouseover="xss(7)" ') do + result = mod.html_attribute('# onmouseover="xss(7)" '.encode(encoding)) + expect(result).to eq '# onmouseover="xss(7)" ' + end + + it %(escapes '/" onerror="xss(9)">') do + result = mod.html_attribute('/" onerror="xss(9)">'.encode(encoding)) + expect(result).to eq '/" onerror="xss(9)">' + end + + it %(escapes '/ onerror="xss(10)"') do + result = mod.html_attribute('/ onerror="xss(10)"'.encode(encoding)) + expect(result).to eq '/ onerror="xss(10)"' + end + + it %(escapes '<') do + result = mod.html_attribute('<'.encode(encoding)) + expect(result).to eq '<<script>xss(14);//<</script>' + end + end + end # tests with encodings + + TEST_INVALID_CHARS.each do |char, _entity| + it "escapes '#{char}'" do + result = mod.html_attribute(char) + expect(result).to eq "&#x#{TEST_REPLACEMENT_CHAR};" + end + end + + it 'escapes tab' do + result = mod.html_attribute("\t") + expect(result).to eq ' ' + end + + it 'escapes return carriage' do + result = mod.html_attribute("\r") + expect(result).to eq ' ' + end + + it 'escapes new line' do + result = mod.html_attribute("\n") + expect(result).to eq ' ' + end + + it 'escapes unicode char' do + result = mod.html_attribute('Ā') + expect(result).to eq 'Ā' + end + + it "doesn't escape ','" do + result = mod.html_attribute(',') + expect(result).to eq ',' + end + + it "doesn't escape '.'" do + result = mod.html_attribute('.') + expect(result).to eq '.' + end + + it "doesn't escape '-'" do + result = mod.html_attribute('-') + expect(result).to eq '-' + end + + it "doesn't escape '_'" do + result = mod.html_attribute('_') + expect(result).to eq '_' + end + + TEST_HTML_ENTITIES.each do |char, entity| + test_name = Hanami::Utils.jruby? ? char.ord : char + + it "escapes #{test_name}" do + result = mod.html_attribute(char) + expect(result).to eq "&#{entity};" + end + end + end # .html_attribute + + describe '.url' do + TEST_ENCODINGS.each do |encoding| + describe encoding.to_s do + it "doesn't escape safe string" do + input = Hanami::Utils::Escape::SafeString.new('javascript:alert(0);') + result = mod.url(input.encode(encoding)) + expect(result).to eq 'javascript:alert(0);' + end + + it 'escapes nil' do + result = mod.url(nil) + expect(result).to eq '' + end + + it "escapes 'test'" do + result = mod.url('test'.encode(encoding)) + expect(result).to eq '' + end + + it "escapes 'http://hanamirb.org'" do + result = mod.url('http://hanamirb.org'.encode(encoding)) + expect(result).to eq 'http://hanamirb.org' + end + + it "escapes 'https://hanamirb.org'" do + result = mod.url('https://hanamirb.org'.encode(encoding)) + expect(result).to eq 'https://hanamirb.org' + end + + it "escapes 'https://hanamirb.org#introduction'" do + result = mod.url('https://hanamirb.org#introduction'.encode(encoding)) + expect(result).to eq 'https://hanamirb.org#introduction' + end + + it "escapes 'https://hanamirb.org/guides/index.html'" do + result = mod.url('https://hanamirb.org/guides/index.html'.encode(encoding)) + expect(result).to eq 'https://hanamirb.org/guides/index.html' + end + + it "escapes 'mailto:user@example.com'" do + result = mod.url('mailto:user@example.com'.encode(encoding)) + expect(result).to eq 'mailto:user@example.com' + end + + it "escapes 'mailto:user@example.com?Subject=Hello'" do + result = mod.url('mailto:user@example.com?Subject=Hello'.encode(encoding)) + expect(result).to eq 'mailto:user@example.com?Subject=Hello' + end + + it "escapes 'javascript:alert(1);'" do + result = mod.url('javascript:alert(1);'.encode(encoding)) + expect(result).to eq '' + end + + # See https://github.com/mzsanford/twitter-text-rb/commit/cffce8e60b7557e9945fc0e8b4383e5a66b1558f + it %(escapes 'http://x.xx/@"style="color:pink"onmouseover=alert(1)//') do + result = mod.url('http://x.xx/@"style="color:pink"onmouseover=alert(1)//'.encode(encoding)) + expect(result).to eq 'http://x.xx/@' + end + + it %{escapes 'http://x.xx/("style="color:red"onmouseover="alert(1)'} do + result = mod.url('http://x.xx/("style="color:red"onmouseover="alert(1)'.encode(encoding)) + expect(result).to eq 'http://x.xx/(' + end + + it %(escapes 'http://x.xx/@%22style=%22color:pink%22onmouseover=alert(1)//') do + result = mod.url('http://x.xx/@%22style=%22color:pink%22onmouseover=alert(1)//'.encode(encoding)) + expect(result).to eq 'http://x.xx/@' + end + end + + describe 'encodes non-String objects that respond to `.to_s`' do + TEST_ENCODINGS.each do |enc| + describe enc.to_s do + it 'escapes a Date' do + result = mod.html(Date.new(2016, 0o1, 27)) + expect(result).to eq '2016-01-27' + end + + it 'escapes a Time' do + time_string = Hanami::Utils.jruby? ? '2016-01-27 12:00:00 UTC' : '2016-01-27 12:00:00 +0000' + result = mod.html(Time.new(2016, 0o1, 27, 12, 0, 0, 0)) + expect(result).to eq time_string + end + + it 'escapes a DateTime' do + result = mod.html(DateTime.new(2016, 0o1, 27, 12, 0, 0, 0)) + expect(result).to eq '2016-01-27T12:00:00+00:00' + end + end + end + end + end + end +end diff --git a/spec/hanami/utils/file_list_spec.rb b/spec/hanami/utils/file_list_spec.rb new file mode 100644 index 0000000..2a151f3 --- /dev/null +++ b/spec/hanami/utils/file_list_spec.rb @@ -0,0 +1,14 @@ +require 'hanami/utils/file_list' + +RSpec.describe Hanami::Utils::FileList do + describe '.[]' do + it 'returns consistent file list across operating systems' do + list = Hanami::Utils::FileList['test/fixtures/file_list/*.rb'] + expect(list).to eq [ + 'test/fixtures/file_list/a.rb', + 'test/fixtures/file_list/aa.rb', + 'test/fixtures/file_list/ab.rb' + ] + end + end +end diff --git a/spec/hanami/utils/hash_spec.rb b/spec/hanami/utils/hash_spec.rb new file mode 100644 index 0000000..9d89743 --- /dev/null +++ b/spec/hanami/utils/hash_spec.rb @@ -0,0 +1,487 @@ +require 'bigdecimal' +require 'hanami/utils/hash' + +RSpec.describe Hanami::Utils::Hash do + describe '#initialize' do + let(:input_to_hash) do + Class.new do + def to_hash + Hash[foo: 'bar'] + end + end.new + end + + let(:input_to_h) do + Class.new do + def to_h + Hash[head: 'tail'] + end + end.new + end + + it 'holds values passed to the constructor' do + hash = Hanami::Utils::Hash.new('foo' => 'bar') + expect(hash['foo']).to eq('bar') + end + + it 'assigns default via block' do + hash = Hanami::Utils::Hash.new { |h, k| h[k] = [] } + hash['foo'].push 'bar' + + expect(hash).to eq('foo' => ['bar']) + end + + it 'accepts a Hanami::Utils::Hash' do + arg = Hanami::Utils::Hash.new('foo' => 'bar') + hash = Hanami::Utils::Hash.new(arg) + + expect(hash.to_h).to be_kind_of(::Hash) + end + + it 'accepts object that implements #to_hash' do + hash = Hanami::Utils::Hash.new(input_to_hash) + + expect(hash.to_h).to eq(input_to_hash.to_hash) + end + + it "raises error when object doesn't implement #to_hash" do + expect { Hanami::Utils::Hash.new(input_to_h) } + .to raise_error(NoMethodError) + end + end + + describe '#symbolize!' do + it 'symbolize keys' do + hash = Hanami::Utils::Hash.new('fub' => 'baz') + hash.symbolize! + + expect(hash['fub']).to be_nil + expect(hash[:fub]).to eq('baz') + end + + it 'does not symbolize nested hashes' do + hash = Hanami::Utils::Hash.new('nested' => { 'key' => 'value' }) + hash.symbolize! + + expect(hash[:nested].keys).to eq(['key']) + end + end + + describe '#deep_symbolize!' do + it 'symbolize keys' do + hash = Hanami::Utils::Hash.new('fub' => 'baz') + hash.deep_symbolize! + + expect(hash['fub']).to be_nil + expect(hash[:fub]).to eq('baz') + end + + it 'symbolizes nested hashes' do + hash = Hanami::Utils::Hash.new('nested' => { 'key' => 'value' }) + hash.deep_symbolize! + + expect(hash[:nested]).to be_kind_of Hanami::Utils::Hash + expect(hash[:nested][:key]).to eq('value') + end + + it 'symbolizes deep nested hashes' do + hash = Hanami::Utils::Hash.new('nested1' => { 'nested2' => { 'nested3' => { 'key' => 1 } } }) + hash.deep_symbolize! + + expect(hash.keys).to eq([:nested1]) + + hash1 = hash[:nested1] + expect(hash1.keys).to eq([:nested2]) + + hash2 = hash1[:nested2] + expect(hash2.keys).to eq([:nested3]) + + hash3 = hash2[:nested3] + expect(hash3.keys).to eq([:key]) + + expect(hash3[:key]).to eq(1) + end + + it 'symbolize nested Hanami::Utils::Hashes' do + nested = Hanami::Utils::Hash.new('key' => 'value') + hash = Hanami::Utils::Hash.new('nested' => nested) + hash.deep_symbolize! + + expect(hash[:nested]).to be_kind_of Hanami::Utils::Hash + expect(hash[:nested][:key]).to eq('value') + end + + it 'symbolize nested object that responds to to_hash' do + nested = Hanami::Utils::Hash.new('metadata' => WrappingHash.new('coverage' => 100)) + nested.deep_symbolize! + + expect(nested[:metadata]).to be_kind_of Hanami::Utils::Hash + expect(nested[:metadata][:coverage]).to eq(100) + end + + it "doesn't try to symbolize nested objects" do + hash = Hanami::Utils::Hash.new('foo' => ['bar']) + hash.deep_symbolize! + + expect(hash[:foo]).to eq(['bar']) + end + end + + describe '#stringify!' do + it 'covert keys to strings' do + hash = Hanami::Utils::Hash.new(fub: 'baz') + hash.stringify! + + expect(hash[:fub]).to be_nil + expect(hash['fub']).to eq('baz') + end + + it 'stringifies nested hashes' do + hash = Hanami::Utils::Hash.new(nested: { key: 'value' }) + hash.stringify! + + expect(hash['nested']).to be_kind_of Hanami::Utils::Hash + expect(hash['nested']['key']).to eq('value') + end + + it 'stringifies nested Hanami::Utils::Hashes' do + nested = Hanami::Utils::Hash.new(key: 'value') + hash = Hanami::Utils::Hash.new(nested: nested) + hash.stringify! + + expect(hash['nested']).to be_kind_of Hanami::Utils::Hash + expect(hash['nested']['key']).to eq('value') + end + + it 'stringifies nested object that responds to to_hash' do + nested = Hanami::Utils::Hash.new(metadata: WrappingHash.new(coverage: 100)) + nested.stringify! + + expect(nested['metadata']).to be_kind_of Hanami::Utils::Hash + expect(nested['metadata']['coverage']).to eq(100) + end + end + + describe '#deep_dup' do + it 'returns an instance of Utils::Hash' do + duped = Hanami::Utils::Hash.new('foo' => 'bar').deep_dup + expect(duped).to be_kind_of(Hanami::Utils::Hash) + end + + it 'returns a hash with duplicated values' do + hash = Hanami::Utils::Hash.new('foo' => 'bar', 'baz' => 'x') + duped = hash.deep_dup + + duped['foo'] = nil + duped['baz'].upcase! + + expect(hash['foo']).to eq('bar') + expect(hash['baz']).to eq('x') + end + + it "doesn't try to duplicate value that can't perform this operation" do + original = { + 'nil' => nil, + 'false' => false, + 'true' => true, + 'symbol' => :symbol, + 'fixnum' => 23, + 'bignum' => 13_289_301_283**2, + 'float' => 1.0, + 'complex' => Complex(0.3), + 'bigdecimal' => BigDecimal.new('12.0001'), + 'rational' => Rational(0.3) + } + + hash = Hanami::Utils::Hash.new(original) + duped = hash.deep_dup + + expect(duped).to eq(original) + expect(duped.object_id).not_to eq(original.object_id) + end + + it 'returns a hash with nested duplicated values' do + hash = Hanami::Utils::Hash.new('foo' => { 'bar' => 'baz' }, 'x' => Hanami::Utils::Hash.new('y' => 'z')) + duped = hash.deep_dup + + duped['foo']['bar'].reverse! + duped['x']['y'].upcase! + + expect(hash['foo']['bar']).to eq('baz') + expect(hash['x']['y']).to eq('z') + end + + it 'preserves original class' do + duped = Hanami::Utils::Hash.new('foo' => {}, 'x' => Hanami::Utils::Hash.new).deep_dup + + expect(duped['foo']).to be_kind_of(::Hash) + expect(duped['x']).to be_kind_of(Hanami::Utils::Hash) + end + end + + describe 'hash interface' do + it 'returns a new Hanami::Utils::Hash for methods which return a ::Hash' do + hash = Hanami::Utils::Hash.new('a' => 1) + result = hash.clear + + expect(hash).to be_empty + expect(result).to be_kind_of(Hanami::Utils::Hash) + end + + it 'returns a value that is compliant with ::Hash return value' do + hash = Hanami::Utils::Hash.new('a' => 1) + result = hash.assoc('a') + + expect(result).to eq ['a', 1] + end + + it 'responds to whatever ::Hash responds to' do + hash = Hanami::Utils::Hash.new('a' => 1) + + expect(hash).to respond_to :rehash + expect(hash).not_to respond_to :unknown_method + end + + it 'accepts blocks for methods' do + hash = Hanami::Utils::Hash.new('a' => 1) + result = hash.delete_if { |k, _| k == 'a' } + + expect(result).to be_empty + end + + describe '#to_h' do + it 'returns a ::Hash' do + actual = Hanami::Utils::Hash.new('a' => 1).to_h + expect(actual).to eq('a' => 1) + end + + it 'returns nested ::Hash' do + hash = { + tutorial: { + instructions: [ + { title: 'foo', body: 'bar' }, + { title: 'hoge', body: 'fuga' } + ] + } + } + + utils_hash = Hanami::Utils::Hash.new(hash) + expect(utils_hash).not_to be_kind_of(::Hash) + + actual = utils_hash.to_h + expect(actual).to eq(hash) + + expect(actual[:tutorial]).to be_kind_of(::Hash) + expect(actual[:tutorial][:instructions]).to all(be_kind_of(::Hash)) + end + + it 'returns nested ::Hash (when symbolized)' do + hash = { + 'tutorial' => { + 'instructions' => [ + { 'title' => 'foo', 'body' => 'bar' }, + { 'title' => 'hoge', 'body' => 'fuga' } + ] + } + } + + utils_hash = Hanami::Utils::Hash.new(hash).deep_symbolize! + expect(utils_hash).not_to be_kind_of(::Hash) + + actual = utils_hash.to_h + expect(actual).to eq(hash) + + expect(actual[:tutorial]).to be_kind_of(::Hash) + expect(actual[:tutorial][:instructions]).to all(be_kind_of(::Hash)) + end + end + + it 'prevents information escape' do + actual = Hanami::Utils::Hash.new('a' => 1) + hash = actual.to_h + hash['b'] = 2 + + expect(actual.to_h).to eq('a' => 1) + end + + it 'prevents information escape for nested hash' + # it 'prevents information escape for nested hash' do + # actual = Hanami::Utils::Hash.new({'a' => {'b' => 2}}) + # hash = actual.to_h + # subhash = hash['a'] + # subhash.merge!('c' => 3) + + # expect(actual.to_h).to eq({'a' => {'b' => 2}}) + # end + + it 'serializes nested objects that respond to to_hash' do + nested = Hanami::Utils::Hash.new(metadata: WrappingHash.new(coverage: 100)) + expect(nested.to_h).to eq(metadata: { coverage: 100 }) + end + end + + describe '#to_hash' do + it 'returns a ::Hash' do + actual = Hanami::Utils::Hash.new('a' => 1).to_hash + expect(actual).to eq('a' => 1) + end + + it 'returns nested ::Hash' do + hash = { + tutorial: { + instructions: [ + { title: 'foo', body: 'bar' }, + { title: 'hoge', body: 'fuga' } + ] + } + } + + utils_hash = Hanami::Utils::Hash.new(hash) + expect(utils_hash).not_to be_kind_of(::Hash) + + actual = utils_hash.to_h + expect(actual).to eq(hash) + + expect(actual[:tutorial]).to be_kind_of(::Hash) + expect(actual[:tutorial][:instructions]).to all(be_kind_of(::Hash)) + end + + it 'returns nested ::Hash (when symbolized)' do + hash = { + 'tutorial' => { + 'instructions' => [ + { 'title' => 'foo', 'body' => 'bar' }, + { 'title' => 'hoge', 'body' => 'fuga' } + ] + } + } + + utils_hash = Hanami::Utils::Hash.new(hash).deep_symbolize! + expect(utils_hash).not_to be_kind_of(::Hash) + + actual = utils_hash.to_h + expect(actual).to eq(hash) + + expect(actual[:tutorial]).to be_kind_of(::Hash) + expect(actual[:tutorial][:instructions]).to all(be_kind_of(::Hash)) + end + + it 'prevents information escape' do + actual = Hanami::Utils::Hash.new('a' => 1) + hash = actual.to_hash + hash['b'] = 2 + + expect(actual.to_hash).to eq('a' => 1) + end + end + + describe '#to_a' do + it 'returns an ::Array' do + actual = Hanami::Utils::Hash.new('a' => 1).to_a + expect(actual).to eq([['a', 1]]) + end + + it 'prevents information escape' do + actual = Hanami::Utils::Hash.new('a' => 1) + array = actual.to_a + array.push(['b', 2]) + + expect(actual.to_a).to eq([['a', 1]]) + end + end + + describe 'equality' do + it 'has a working equality' do + hash = Hanami::Utils::Hash.new('a' => 1) + other = Hanami::Utils::Hash.new('a' => 1) + + expect(hash == other).to be_truthy + end + + it 'has a working equality with raw hashes' do + hash = Hanami::Utils::Hash.new('a' => 1) + expect(hash == { 'a' => 1 }).to be_truthy + end + end + + describe 'case equality' do + it 'has a working case equality' do + hash = Hanami::Utils::Hash.new('a' => 1) + other = Hanami::Utils::Hash.new('a' => 1) + + expect(hash === other).to be_truthy # rubocop:disable Style/CaseEquality + end + + it 'has a working case equality with raw hashes' do + hash = Hanami::Utils::Hash.new('a' => 1) + expect(hash === { 'a' => 1 }).to be_truthy # rubocop:disable Style/CaseEquality + end + end + + describe 'value equality' do + it 'has a working value equality' do + hash = Hanami::Utils::Hash.new('a' => 1) + other = Hanami::Utils::Hash.new('a' => 1) + + expect(hash).to eql(other) + end + + it 'has a working value equality with raw hashes' do + hash = Hanami::Utils::Hash.new('a' => 1) + expect(hash).to eql('a' => 1) + end + end + + describe 'identity equality' do + it 'has a working identity equality' do + hash = Hanami::Utils::Hash.new('a' => 1) + expect(hash).to equal(hash) + end + + it 'has a working identity equality with raw hashes' do + hash = Hanami::Utils::Hash.new('a' => 1) + expect(hash).not_to equal('a' => 1) + end + end + + describe '#hash' do + it 'returns the same hash result of ::Hash' do + expected = { 'l' => 23 }.hash + actual = Hanami::Utils::Hash.new('l' => 23).hash + + expect(actual).to eq expected + end + end + + describe '#inspect' do + it 'returns the same output of ::Hash' do + expected = { 'l' => 23, l: 23 }.inspect + actual = Hanami::Utils::Hash.new('l' => 23, l: 23).inspect + + expect(actual).to eq expected + end + end + + describe 'unknown method' do + it 'raises error' do + begin + Hanami::Utils::Hash.new('l' => 23).party! + rescue NoMethodError => e + expect(e.message).to eq %(undefined method `party!' for {\"l\"=>23}:Hanami::Utils::Hash) + end + end + + # See: https://github.com/hanami/utils/issues/48 + it 'returns the correct object when a NoMethodError is raised' do + hash = Hanami::Utils::Hash.new('a' => 1) + + if RUBY_VERSION == '2.4.0' # rubocop:disable Style/ConditionalAssignment + exception_message = "undefined method `foo' for 1:Integer" + else + exception_message = "undefined method `foo' for 1:Fixnum" + end + + expect { hash.all? { |_, v| v.foo } }.to raise_error(NoMethodError, include(exception_message)) + end + end +end diff --git a/spec/hanami/utils/inflector_spec.rb b/spec/hanami/utils/inflector_spec.rb new file mode 100644 index 0000000..1da984c --- /dev/null +++ b/spec/hanami/utils/inflector_spec.rb @@ -0,0 +1,140 @@ +require 'hanami/utils/inflector' +require 'hanami/utils/string' + +RSpec.describe Hanami::Utils::Inflector do + describe '.inflections' do + it 'adds exception for singular rule' do + actual = Hanami::Utils::Inflector.singularize('analyses') # see spec/support/fixtures.rb + expect(actual).to eq 'analysis' + + actual = Hanami::Utils::Inflector.singularize('algae') # see spec/support/fixtures.rb + expect(actual).to eq 'alga' + end + + it 'adds exception for plural rule' do + actual = Hanami::Utils::Inflector.pluralize('analysis') # see spec/support/fixtures.rb + expect(actual).to eq 'analyses' + + actual = Hanami::Utils::Inflector.pluralize('alga') # see spec/support/fixtures.rb + expect(actual).to eq 'algae' + end + + it 'adds exception for uncountable rule' do + actual = Hanami::Utils::Inflector.pluralize('music') # see spec/support/fixtures.rb + expect(actual).to eq 'music' + + actual = Hanami::Utils::Inflector.singularize('music') # see spec/support/fixtures.rb + expect(actual).to eq 'music' + + actual = Hanami::Utils::Inflector.pluralize('butter') # see spec/support/fixtures.rb + expect(actual).to eq 'butter' + + actual = Hanami::Utils::Inflector.singularize('butter') # see spec/support/fixtures.rb + expect(actual).to eq 'butter' + end + end + + describe '.pluralize' do + it 'returns nil when nil is given' do + actual = Hanami::Utils::Inflector.pluralize(nil) + expect(actual).to be_nil + end + + it 'returns empty string when empty string is given' do + actual = Hanami::Utils::Inflector.pluralize('') + expect(actual).to be_empty + end + + it 'returns empty string when empty string is given (multiple chars)' do + actual = Hanami::Utils::Inflector.pluralize(string = ' ') + expect(actual).to eq string + end + + it 'returns instance of String' do + result = Hanami::Utils::Inflector.pluralize('Hanami') + expect(result.class).to eq ::String + end + + it "doesn't modify original string" do + string = 'application' + result = Hanami::Utils::Inflector.pluralize(string) + + expect(result.object_id).not_to eq(string.object_id) + expect(string).to eq('application') + end + + TEST_PLURALS.each do |singular, plural| + it %(pluralizes "#{singular}" to "#{plural}") do + actual = Hanami::Utils::Inflector.pluralize(singular) + expect(actual).to eq plural + end + + it %(pluralizes titleized "#{Hanami::Utils::String.new(singular).titleize}" to "#{plural}") do + actual = Hanami::Utils::Inflector.pluralize(Hanami::Utils::String.new(singular).titleize) + expect(actual).to eq Hanami::Utils::String.new(plural).titleize + end + + # it %(doesn't pluralize "#{ plural }" as it's already plural) do + # actual = Hanami::Utils::Inflector.pluralize(plural) + # expect(actual).to eq plural + # end + + # it %(doesn't pluralize titleized "#{ Hanami::Utils::String.new(singular).titleize }" as it's already plural) do + # actual = Hanami::Utils::Inflector.pluralize(Hanami::Utils::String.new(plural).titleize) + # expect(actual).to eq Hanami::Utils::String.new(plural).titleize + # end + end + end + + describe '.singularize' do + it 'returns nil when nil is given' do + actual = Hanami::Utils::Inflector.singularize(nil) + expect(actual).to be_nil + end + + it 'returns empty string when empty string is given' do + actual = Hanami::Utils::Inflector.singularize('') + expect(actual).to be_empty + end + + it 'returns empty string when empty string is given (multiple chars)' do + actual = Hanami::Utils::Inflector.singularize(string = ' ') + expect(actual).to eq string + end + + it 'returns instance of String' do + result = Hanami::Utils::Inflector.singularize('application') + expect(result.class).to eq ::String + end + + it "doesn't modify original string" do + string = 'applications' + result = Hanami::Utils::Inflector.singularize(string) + + expect(result.object_id).not_to eq(string.object_id) + expect(string).to eq('applications') + end + + TEST_SINGULARS.each do |singular, plural| + it %(singularizes "#{plural}" to "#{singular}") do + actual = Hanami::Utils::Inflector.singularize(plural) + expect(actual).to eq singular + end + + it %(singularizes titleized "#{Hanami::Utils::String.new(plural).titleize}" to "#{singular}") do + actual = Hanami::Utils::Inflector.singularize(Hanami::Utils::String.new(plural).titleize) + expect(actual).to eq Hanami::Utils::String.new(singular).titleize + end + + # it %(doesn't singularizes "#{ singular }" as it's already singular) do + # actual = Hanami::Utils::Inflector.singularize(singular) + # expect(actual).to eq singular + # end + + # it %(doesn't singularizes titleized "#{ Hanami::Utils::String.new(plural).titleize }" as it's already singular) do + # actual = Hanami::Utils::Inflector.singularize(Hanami::Utils::String.new(singular).titleize) + # expect(actual).to Hanami::Utils::String.new(singular).titleize + # end + end + end +end diff --git a/spec/hanami/utils/io_spec.rb b/spec/hanami/utils/io_spec.rb new file mode 100644 index 0000000..dd2aa66 --- /dev/null +++ b/spec/hanami/utils/io_spec.rb @@ -0,0 +1,17 @@ +require 'hanami/utils/io' + +class IOTest + TEST_CONSTANT = 'initial'.freeze +end + +RSpec.describe Hanami::Utils::IO do + describe '.silence_warnings' do + it 'lowers verbosity of stdout' do + expect do + Hanami::Utils::IO.silence_warnings do + IOTest::TEST_CONSTANT = 'redefined'.freeze + end + end.to output(eq('')).to_stderr + end + end +end diff --git a/spec/hanami/utils/kernel_spec.rb b/spec/hanami/utils/kernel_spec.rb new file mode 100644 index 0000000..ed57d93 --- /dev/null +++ b/spec/hanami/utils/kernel_spec.rb @@ -0,0 +1,2625 @@ +require 'ostruct' +require 'bigdecimal' +require 'securerandom' +require 'hanami/utils/kernel' + +RSpec.describe Hanami::Utils::Kernel do + describe '.Array' do + describe 'successful operations' do + before do + ResultSet = Struct.new(:records) do + def to_a + records.to_a.sort + end + end + + Response = Struct.new(:status, :headers, :body) do + def to_ary + [status, headers, body] + end + end + + @result = Hanami::Utils::Kernel.Array(input) + end + + after do + Object.send(:remove_const, :ResultSet) + Object.send(:remove_const, :Response) + end + + describe 'when nil is given' do + let(:input) { nil } + + it 'returns an empty array' do + expect(@result).to eq [] + end + end + + describe 'when true is given' do + let(:input) { true } + + it 'returns an empty array' do + expect(@result).to eq [true] + end + end + + describe 'when false is given' do + let(:input) { false } + + it 'returns an empty array' do + expect(@result).to eq [false] + end + end + + describe 'when an object is given' do + let(:input) { Object.new } + + it 'returns an array' do + expect(@result).to eq [input] + end + end + + describe 'when an array is given' do + let(:input) { [Object.new] } + + it 'returns an array' do + expect(@result).to eq input + end + end + + describe 'when a nested array is given' do + let(:input) { [1, [2, 3]] } + + it 'returns a flatten array' do + expect(@result).to eq [1, 2, 3] + end + + it "doesn't change the argument" do + expect(input).to eq [1, [2, 3]] + end + end + + describe 'when an array with nil values is given' do + let(:input) { [1, [nil, 3]] } + + it 'returns a compacted array' do + expect(@result).to eq [1, 3] + end + end + + describe 'when an array with duplicated values is given' do + let(:input) { [2, [2, 3]] } + + it 'returns an array with uniq values' do + expect(@result).to eq [2, 3] + end + + it "doesn't change the argument" do + expect(input).to eq [2, [2, 3]] + end + end + + describe 'when a set is given' do + let(:input) { Set.new([33, 12]) } + + it 'returns an array with uniq values' do + expect(@result).to eq [33, 12] + end + end + + describe 'when a object that implements #to_a is given' do + let(:input) { ResultSet.new([2, 1, 3]) } + + it 'returns an array' do + expect(@result).to eq [1, 2, 3] + end + end + + describe 'when a object that implements #to_ary is given' do + let(:input) { Response.new(200, {}, 'hello') } + + it 'returns an array' do + expect(@result).to eq [200, {}, 'hello'] + end + end + end + end + + describe '.Set' do + before do + UuidSet = Class.new do + def initialize(*uuids) + @uuids = uuids + end + + def to_set + Set.new.tap do |set| + @uuids.each { |uuid| set.add(uuid) } + end + end + end + + BaseObject = Class.new(BasicObject) do + def nil? + false + end + end + end + + after do + Object.send(:remove_const, :UuidSet) + Object.send(:remove_const, :BaseObject) + end + + describe 'successful operations' do + before do + @result = Hanami::Utils::Kernel.Set(input) + end + + describe 'when nil is given' do + let(:input) { nil } + + it 'returns an empty set' do + expect(@result).to eq Set.new + end + end + + describe 'when true is given' do + let(:input) { true } + + it 'returns a set' do + expect(@result).to eq Set.new([true]) + end + end + + describe 'when false is given' do + let(:input) { false } + + it 'returns a set' do + expect(@result).to eq Set.new([false]) + end + end + + describe 'when an object is given' do + let(:input) { Object.new } + + it 'returns an set' do + expect(@result).to eq Set.new([input]) + end + end + + describe 'when an array is given' do + let(:input) { [1] } + + it 'returns an set' do + expect(@result).to eq Set.new(input) + end + end + + describe 'when an hash is given' do + let(:input) { Hash[a: 1] } + + it 'returns an set' do + expect(@result).to eq Set.new([[:a, 1]]) + end + end + + describe 'when a set is given' do + let(:input) { Set.new([Object.new]) } + + it 'returns self' do + expect(@result).to eq input + end + end + + describe 'when a nested array is given' do + let(:input) { [1, [2, 3]] } + + it 'returns it wraps in a set' do + expect(@result).to eq Set.new([1, [2, 3]]) + end + end + + describe 'when an array with nil values is given' do + let(:input) { [1, [nil, 3]] } + + it 'returns it wraps in a set' do + expect(@result).to eq Set.new([1, [nil, 3]]) + end + end + + describe 'when an set with duplicated values is given' do + let(:input) { [2, 3, 3] } + + it 'returns an set with uniq values' do + expect(@result).to eq Set.new([2, 3]) + end + end + + describe 'when an set with nested duplicated values is given' do + let(:input) { [2, [2, 3]] } + + it 'returns it wraps in a set' do + expect(@result).to eq Set.new([2, [2, 3]]) + end + end + + describe 'when a object that implements #to_set is given' do + let(:input) { UuidSet.new(*args) } + let(:args) { [SecureRandom.uuid, SecureRandom.uuid] } + + it 'returns an set' do + expect(@result).to eq Set.new(args) + end + end + end + + describe 'failure operations' do + describe "when a an object that doesn't implement #respond_to?" do + let(:input) { BaseObject.new } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Set(input) }.to raise_error(TypeError) + end + + it 'returns informations about the failure' do + begin + Hanami::Utils::Kernel.Set(input) + rescue => e + expect(e.message).to match "can't convert into Set" + end + end + end + end + end + + describe '.Hash' do + before do + Room = Class.new do + def initialize(*args) + @args = args + end + + def to_h + Hash[*@args] + end + end + + Record = Class.new do + def initialize(attributes = {}) + @attributes = attributes + end + + def to_hash + @attributes + end + end + + BaseObject = Class.new(BasicObject) do + def nil? + false + end + end + end + + after do + Object.send(:remove_const, :Room) + Object.send(:remove_const, :Record) + Object.send(:remove_const, :BaseObject) + end + + describe 'successful operations' do + before do + @result = Hanami::Utils::Kernel.Hash(input) + end + + describe 'when nil is given' do + let(:input) { nil } + + it 'returns an empty hash' do + expect(@result).to eq({}) + end + end + + describe 'when an hash is given' do + let(:input) { Hash[l: 23] } + + it 'returns the input' do + expect(@result).to eq input + end + end + + describe 'when an array is given' do + let(:input) { [[:a, 1]] } + + it 'returns an hash' do + expect(@result).to eq Hash[a: 1] + end + end + + describe 'when a set is given' do + let(:input) { Set.new([['x', 12]]) } + + it 'returns an hash' do + expect(@result).to eq Hash['x' => 12] + end + end + + describe 'when a object that implements #to_h is given' do + let(:input) { Room.new(:key, 123_456) } + + it 'returns an hash' do + expect(@result).to eq Hash[key: 123_456] + end + end + + describe 'when a object that implements #to_hash is given' do + let(:input) { Record.new(name: 'L') } + + it 'returns an hash' do + expect(@result).to eq Hash[name: 'L'] + end + end + end + + describe 'failure operations' do + describe "when a an object that doesn't implement #respond_to?" do + let(:input) { BaseObject.new } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Hash(input) }.to raise_error(TypeError) + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Hash(input) + rescue => e + expect(e.message).to eq "can't convert into Hash" + end + end + end + + describe 'when true is given' do + let(:input) { true } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Hash(input) }.to raise_error(TypeError) + end + end + + describe 'when false is given' do + let(:input) { false } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Hash(input) }.to raise_error(TypeError) + end + end + + describe 'when an array with one element is given' do + let(:input) { [1] } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Hash(input) }.to raise_error(TypeError) + end + end + + describe 'when an array with two elements is given' do + let(:input) { [:a, 1] } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Hash(input) }.to raise_error(TypeError) + end + end + + describe 'when a set of one element is given' do + let(:input) { Set.new([1]) } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Hash(input) }.to raise_error(TypeError) + end + end + + describe 'when a set of two elements is given' do + let(:input) { Set.new([:a, 1]) } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Hash(input) }.to raise_error(TypeError) + end + end + end + end + + describe '.Integer' do + describe 'successful operations' do + before do + PersonFavNumber = Struct.new(:name) do + def to_int + 23 + end + end + + @result = Hanami::Utils::Kernel.Integer(input) + end + + after do + Object.send(:remove_const, :PersonFavNumber) + end + + describe 'when nil is given' do + let(:input) { nil } + + it 'returns 0' do + expect(@result).to eq 0 + end + end + + describe 'when a fixnum given' do + let(:input) { 1 } + + it 'returns an integer' do + expect(@result).to eq 1 + end + end + + describe 'when a float is given' do + let(:input) { 1.2 } + + it 'returns an integer' do + expect(@result).to eq 1 + end + end + + describe 'when a string given' do + let(:input) { '23' } + + it 'returns an integer' do + expect(@result).to eq 23 + end + end + + describe 'when a string representing a float number is given' do + let(:input) { '23.4' } + + it 'returns an integer' do + expect(@result).to eq 23 + end + end + + describe 'when an octal is given' do + let(:input) { 0o11 } + + it 'returns the string representation' do + expect(@result).to eq 9 + end + end + + describe 'when a hex is given' do + let(:input) { 0xf5 } + + it 'returns the string representation' do + expect(@result).to eq 245 + end + end + + describe 'when a bignum is given' do + let(:input) { 13_289_301_283**2 } + + it 'returns an bignum' do + expect(@result).to eq 176_605_528_590_345_446_089 + end + end + + describe 'when a bigdecimal is given' do + let(:input) { BigDecimal.new('12.0001') } + + it 'returns an bignum' do + expect(@result).to eq 12 + end + end + + describe 'when a complex number is given' do + let(:input) { Complex(0.3) } + + it 'returns an integer' do + expect(@result).to eq 0 + end + end + + describe 'when a string representing a complex number is given' do + let(:input) { '2.5/1' } + + it 'returns an integer' do + expect(@result).to eq 2 + end + end + + describe 'when a rational number is given' do + let(:input) { Rational(0.3) } + + it 'returns an integer' do + expect(@result).to eq 0 + end + end + + describe 'when a string representing a rational number is given' do + let(:input) { '2/3' } + + it 'returns an integer' do + expect(@result).to eq 2 + end + end + + describe 'when a time is given' do + let(:input) { Time.at(0).utc } + + it 'returns the string representation' do + expect(@result).to eq 0 + end + end + + describe 'when an object that implements #to_int is given' do + let(:input) { PersonFavNumber.new('Luca') } + + it 'returns an integer' do + expect(@result).to eq 23 + end + end + end + + describe 'failure operations' do + describe 'true is given' do + let(:input) { true } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Integer(input) }.to raise_error(TypeError) + end + end + + describe 'false is given' do + let(:input) { false } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Integer(input) }.to raise_error(TypeError) + end + end + + describe 'when a date is given' do + let(:input) { Date.today } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Integer(input) }.to raise_error(TypeError) + end + end + + describe 'when a datetime is given' do + let(:input) { DateTime.now } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Integer(input) }.to raise_error(TypeError) + end + end + + describe "when a an object that doesn't implement #respond_to? " do + let(:input) { BasicObject.new } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Integer(input) }.to raise_error(TypeError) + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Integer(input) + rescue => e + expect(e.message).to eq "can't convert into Integer" + end + end + end + + describe "when a an object that doesn't implement any integer interface" do + let(:input) { OpenStruct.new(color: 'purple') } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Integer(input) }.to raise_error(TypeError) + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Integer(input) + rescue => e + expect(e.message).to eq "can't convert #{input.inspect} into Integer" + end + end + end + + describe 'when a bigdecimal infinity is given' do + let(:input) { BigDecimal.new('Infinity') } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Integer(input) }.to raise_error(TypeError) + end + end + + describe 'when a bigdecimal NaN is given' do + let(:input) { BigDecimal.new('NaN') } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Integer(input) }.to raise_error(TypeError) + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Integer(input) + rescue => e + expect(e.message).to eq "can't convert #{input.inspect} into Integer" + end + end + end + + describe 'when a big complex number is given' do + let(:input) { Complex(2, 3) } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Integer(input) }.to raise_error(TypeError) + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Integer(input) + rescue => e + expect(e.message).to eq "can't convert #{input.inspect} into Integer" + end + end + end + + describe 'when a big rational number is given' do + let(:input) { Rational(-8)**Rational(1, 3) } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Integer(input) }.to raise_error(TypeError) + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Integer(input) + rescue => e + expect(e.message).to eq "can't convert #{input.inspect} into Integer" + end + end + end + + describe 'when a string without numbers is given' do + let(:input) { 'home' } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Integer(input) } + .to raise_error(TypeError, "can't convert #{input.inspect} into Integer") + end + end + + describe 'when a string which starts with an integer is given' do + let(:input) { '23 street' } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Integer(input) } + .to raise_error(TypeError, "can't convert #{input.inspect} into Integer") + end + end + end + end + + describe '.BigDecimal' do + describe 'successful operations' do + before do + PersonFavDecimalNumber = Struct.new(:name) do + def to_d + BigDecimal.new(Rational(23).to_s) + end + end + + @result = Hanami::Utils::Kernel.BigDecimal(input) + end + + after do + Object.send(:remove_const, :PersonFavDecimalNumber) + end + + describe 'when a fixnum given' do + let(:input) { 1 } + + it 'returns a BigDecimal' do + expect(@result).to eq BigDecimal.new(1) + end + end + + describe 'when a float is given' do + let(:input) { 1.2 } + + it 'returns a BigDecimal' do + expect(@result).to eq BigDecimal.new('1.2') + end + end + + describe 'when a string given' do + let(:input) { '23' } + + it 'returns a BigDecimal' do + expect(@result).to eq BigDecimal.new(23) + end + end + + describe 'when a string representing a float number is given' do + let(:input) { '23.1' } + + it 'returns a BigDecimal' do + expect(@result).to eq BigDecimal.new('23.1') + end + end + + describe 'when an octal is given' do + let(:input) { 0o11 } + + it 'returns a BigDecimal' do + expect(@result).to eq BigDecimal.new(9) + end + end + + describe 'when a hex is given' do + let(:input) { 0xf5 } + + it 'returns a BigDecimal' do + expect(@result).to eq BigDecimal.new(245) + end + end + + describe 'when a bignum is given' do + let(:input) { 13_289_301_283**2 } + + it 'returns a BigDecimal' do + expect(@result).to eq BigDecimal.new(176_605_528_590_345_446_089) + end + end + + describe 'when a bigdecimal is given' do + let(:input) { BigDecimal.new('12.0001') } + + it 'returns a BigDecimal' do + expect(@result).to eq BigDecimal.new('12.0001') + end + end + + describe 'when a complex number with imaginary part is given' do + let(:input) { Complex(758.3, 0) } + + it 'returns a BigDecimal' do + expect(@result).to eq BigDecimal.new('758.3') + end + end + + describe 'when a complex number without imaginary part is given' do + let(:input) { Complex(0.3) } + + it 'returns a BigDecimal' do + expect(@result).to eq BigDecimal.new('0.3') + end + end + + describe 'when a string representing a complex number is given' do + let(:input) { '2.5/1' } + + it 'returns a BigDecimal' do + expect(@result).to eq BigDecimal.new('2.5') + end + end + + describe 'when a rational number is given' do + let(:input) { Rational(0.3) } + + it 'returns an BigDecimal' do + expect(@result).to eq BigDecimal.new(input.to_s) + end + end + + describe 'when a string representing a rational number is given' do + let(:input) { '2/3' } + + it 'returns a BigDecimal' do + expect(@result).to eq BigDecimal.new('2/3') + end + end + + describe 'when a BigDecimal NaN is given' do + let(:input) { 'NaN' } + + it 'returns a BigDecimal NaN' do + expect(@result).to be_kind_of BigDecimal + expect(@result).to be_nan + end + end + + describe 'when a BigDecimal Infinity (string) is given' do + let(:input) { 'Infinity' } + + it 'returns a BigDecimal Infinity' do + expect(@result).to be_kind_of BigDecimal + expect(@result).to be_infinite + end + end + + describe 'when a BigDecimal Infinity is given' do + let(:input) { BigDecimal.new('Infinity') } + + it 'returns the BigDecimal Infinity representation' do + expect(@result).to eq BigDecimal.new('Infinity') + end + end + + describe 'when an object that implements #to_d is given' do + let(:input) { PersonFavDecimalNumber.new('Luca') } + + it 'returns a BigDecimal' do + expect(@result).to eq BigDecimal.new(23) + end + end + end + + describe 'when a string without numbers is given' do + let(:input) { 'home' } + + if RUBY_VERSION == '2.4.0' + it 'raises error' do + expect { Hanami::Utils::Kernel.BigDecimal(input) }.to raise_error(ArgumentError, "invalid value for BigDecimal(): #{input.inspect}") + end + else + it 'returns an BigDecimal' do + expect(Hanami::Utils::Kernel.BigDecimal(input)).to eq BigDecimal.new(0) + end + end + end + + describe 'when a string which starts with a big decimal is given' do + let(:input) { '23.0 street' } + + it 'returns an BigDecimal' do + expect(Hanami::Utils::Kernel.BigDecimal(input)).to eq BigDecimal.new(23) + end + end + + # Bug: https://github.com/hanami/utils/issues/140 + describe 'when a negative bigdecimal is given' do + let(:input) { BigDecimal.new('-12.0001') } + + it 'returns a BigDecimal' do + expect(Hanami::Utils::Kernel.BigDecimal(input)).to eq BigDecimal.new('-12.0001') + end + end + + # Bug: https://github.com/hanami/utils/issues/140 + describe 'when the big decimal is less than 1 with high precision' do + let(:input) { BigDecimal.new('0.0001') } + + it 'returns a BigDecimal' do + expect(Hanami::Utils::Kernel.BigDecimal(input)).to eq BigDecimal.new('0.0001') + end + end + + describe 'failure operations' do + describe 'when nil is given' do + let(:input) { nil } + + it 'raises error' do + expect { Hanami::Utils::Kernel.BigDecimal(input) }.to raise_error(TypeError) + end + end + + describe 'when a true is given' do + let(:input) { true } + + it 'raises error' do + expect { Hanami::Utils::Kernel.BigDecimal(input) }.to raise_error(TypeError) + end + end + + describe 'when a false is given' do + let(:input) { false } + + it 'raises error' do + expect { Hanami::Utils::Kernel.BigDecimal(input) }.to raise_error(TypeError) + end + end + + describe 'when a date is given' do + let(:input) { Date.today } + + it 'raises error' do + expect { Hanami::Utils::Kernel.BigDecimal(input) }.to raise_error(TypeError) + end + end + + describe 'when a datetime is given' do + let(:input) { DateTime.now } + + it 'raises error' do + expect { Hanami::Utils::Kernel.BigDecimal(input) }.to raise_error(TypeError) + end + end + + describe 'when a time is given' do + let(:input) { Time.now } + + it 'raises error' do + expect { Hanami::Utils::Kernel.BigDecimal(input) }.to raise_error(TypeError) + end + end + + describe "when a an object that doesn't implement #respond_to? " do + let(:input) { BasicObject.new } + + it 'raises error' do + expect { Hanami::Utils::Kernel.BigDecimal(input) }.to raise_error(TypeError) + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.BigDecimal(input) + rescue => e + expect(e.message).to eq "can't convert into BigDecimal" + end + end + end + end + end + + describe '.Float' do + describe 'successful operations' do + before do + class Pi + def to_f + 3.14 + end + end + + @result = Hanami::Utils::Kernel.Float(input) + end + + after do + Object.send(:remove_const, :Pi) + end + + describe 'when nil is given' do + let(:input) { nil } + + it 'returns 0.0' do + expect(@result).to eq 0.0 + end + end + + describe 'when a float is given' do + let(:input) { 1.2 } + + it 'returns the argument' do + expect(@result).to eq 1.2 + end + end + + describe 'when a fixnum given' do + let(:input) { 1 } + + it 'returns a float' do + expect(@result).to be_kind_of(Float) + expect(@result).to eq 1.0 + end + end + + describe 'when a string given' do + let(:input) { '23' } + + it 'returns a float' do + expect(@result).to be_kind_of(Float) + expect(@result).to eq 23.0 + end + end + + describe 'when a string representing a float number is given' do + let(:input) { '23.4' } + + it 'returns a float' do + expect(@result).to be_kind_of(Float) + expect(@result).to eq 23.4 + end + end + + describe 'when an octal is given' do + let(:input) { 0o11 } + + it 'returns the base 10 float' do + expect(@result).to eq 9.0 + end + end + + describe 'when a hex is given' do + let(:input) { 0xf5 } + + it 'returns the base 10 float' do + expect(@result).to eq 245.0 + end + end + + describe 'when a bignum is given' do + let(:input) { 13_289_301_283**2 } + + it 'returns a float' do + expect(@result).to be_kind_of(Float) + expect(@result).to eq 176_605_528_590_345_446_089.0 + end + end + + describe 'when a bigdecimal is given' do + let(:input) { BigDecimal.new('12.0001') } + + it 'returns a float' do + expect(@result).to be_kind_of(Float) + expect(@result).to eq 12.0001 + end + end + + describe 'when a bigdecimal infinity is given' do + let(:input) { BigDecimal.new('Infinity') } + + it 'returns Infinity' do + expect(@result).to be_kind_of(Float) + expect(@result.to_s).to eq 'Infinity' + end + end + + describe 'when a bigdecimal NaN is given' do + let(:input) { BigDecimal.new('NaN') } + + it 'returns NaN' do + expect(@result).to be_kind_of(Float) + expect(@result.to_s).to eq 'NaN' + end + end + + describe 'when a complex number is given' do + let(:input) { Complex(0.3) } + + it 'returns a float' do + expect(@result).to be_kind_of(Float) + expect(@result).to eq 0.3 + end + end + + describe 'when a string representing a complex number is given' do + let(:input) { '2.5/1' } + + it 'returns a float' do + expect(@result).to be_kind_of(Float) + expect(@result).to eq 2.5 + end + end + + describe 'when a rational number is given' do + let(:input) { Rational(0.3) } + + it 'returns a float' do + expect(@result).to be_kind_of(Float) + expect(@result).to eq 0.3 + end + end + + describe 'when a string representing a rational number is given' do + let(:input) { '2/3' } + + it 'returns a float' do + expect(@result).to be_kind_of(Float) + expect(@result).to eq 2.0 + end + end + + describe 'when a time is given' do + let(:input) { Time.at(0).utc } + + it 'returns the float representation' do + expect(@result).to be_kind_of(Float) + expect(@result).to eq 0.0 + end + end + + describe 'when an object that implements #to_int is given' do + let(:input) { Pi.new } + + it 'returns a float' do + expect(@result).to be_kind_of(Float) + expect(@result).to eq 3.14 + end + end + end + + describe 'failure operations' do + describe 'true is given' do + let(:input) { true } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Float(input) }.to raise_error(TypeError) + end + end + + describe 'false is given' do + let(:input) { false } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Float(input) }.to raise_error(TypeError) + end + end + + describe 'when a date is given' do + let(:input) { Date.today } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Float(input) }.to raise_error(TypeError) + end + end + + describe 'when a datetime is given' do + let(:input) { DateTime.now } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Float(input) }.to raise_error(TypeError) + end + end + + describe 'when a string without numbers is given' do + let(:input) { 'home' } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Float(input) } + .to raise_error(TypeError, "can't convert #{input.inspect} into Float") + end + end + + describe 'when a string which starts with a float is given' do + let(:input) { '23.0 street' } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Float(input) } + .to raise_error(TypeError, "can't convert #{input.inspect} into Float") + end + end + + describe "when a an object that doesn't implement #respond_to? " do + let(:input) { BasicObject.new } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Float(input) }.to raise_error(TypeError) + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Float(input) + rescue => e + expect(e.message).to eq "can't convert into Float" + end + end + end + + describe "when a an object that doesn't implement any float interface" do + let(:input) { OpenStruct.new(color: 'purple') } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Float(input) }.to raise_error(TypeError) + end + end + + describe 'when a big complex number is given' do + let(:input) { Complex(2, 3) } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Float(input) }.to raise_error(TypeError) + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Float(input) + rescue => e + expect(e.message).to eq "can't convert #{input.inspect} into Float" + end + end + end + + describe 'when a big rational number is given' do + let(:input) { Rational(-8)**Rational(1, 3) } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Float(input) }.to raise_error(TypeError) + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Float(input) + rescue => e + expect(e.message).to eq "can't convert #{input.inspect} into Float" + end + end + end + end + end + + describe '.String' do + before do + Book = Struct.new(:title) + + SimpleObject = Class.new(BasicObject) do + def respond_to?(method_name, _include_private = false) + method_name.to_sym == :to_s + end + + def to_s + 'simple object' + end + end + + Isbn = Struct.new(:code) do + def to_str + code.to_s + end + end + end + + after do + Object.send(:remove_const, :Book) + Object.send(:remove_const, :SimpleObject) + Object.send(:remove_const, :Isbn) + end + + describe 'successful operations' do + before do + @result = Hanami::Utils::Kernel.String(input) + end + + describe 'when nil is given' do + let(:input) { nil } + + it 'returns empty string' do + expect(@result).to eq '' + end + end + + describe 'when true is given' do + let(:input) { true } + + it 'returns nil' do + expect(@result).to eq 'true' + end + end + + describe 'when false is given' do + let(:input) { false } + + it 'returns nil' do + expect(@result).to eq 'false' + end + end + + describe 'when an empty string is given' do + let(:input) { '' } + + it 'returns it' do + expect(@result).to eq '' + end + end + + describe 'when a string is given' do + let(:input) { 'ciao' } + + it 'returns it' do + expect(@result).to eq 'ciao' + end + end + + describe 'when an integer is given' do + let(:input) { 23 } + + it 'returns the string representation' do + expect(@result).to eq '23' + end + end + + describe 'when a float is given' do + let(:input) { 3.14 } + + it 'returns the string representation' do + expect(@result).to eq '3.14' + end + end + + describe 'when an octal is given' do + let(:input) { 0o13 } + + it 'returns the string representation' do + expect(@result).to eq '11' + end + end + + describe 'when a hex is given' do + let(:input) { 0xc0ff33 } + + it 'returns the string representation' do + expect(@result).to eq '12648243' + end + end + + describe 'when a big decimal is given' do + let(:input) { BigDecimal.new(7944.2343, 10) } + + if RUBY_VERSION == '2.4.0' + it 'returns the string representation' do + expect(@result).to eq '0.79442343e4' + end + else + it 'returns the string representation' do + expect(@result).to eq '0.79442343E4' + end + end + end + + describe 'when a big decimal infinity is given' do + let(:input) { BigDecimal.new('Infinity') } + + it 'returns the string representation' do + expect(@result).to eq 'Infinity' + end + end + + describe 'when a big decimal NaN is given' do + let(:input) { BigDecimal.new('NaN') } + + it 'returns the string representation' do + expect(@result).to eq 'NaN' + end + end + + describe 'when a complex is given' do + let(:input) { Complex(11, 2) } + + it 'returns the string representation' do + expect(@result).to eq '11+2i' + end + end + + describe 'when a rational is given' do + let(:input) { Rational(-22) } + + it 'returns the string representation' do + expect(@result).to eq '-22/1' + end + end + + describe 'when an empty array is given' do + let(:input) { [] } + + it 'returns the string representation' do + expect(@result).to eq '[]' + end + end + + describe 'when an array of integers is given' do + let(:input) { [1, 2, 3] } + + it 'returns the string representation' do + expect(@result).to eq '[1, 2, 3]' + end + end + + describe 'when an array of strings is given' do + let(:input) { %w(a b c) } + + it 'returns the string representation' do + expect(@result).to eq '["a", "b", "c"]' + end + end + + describe 'when an array of objects is given' do + let(:input) { [Object.new] } + + it 'returns the string representation' do + expect(@result).to include'[# 2 } } + + it 'returns the string representation' do + expect(@result).to eq '{:a=>1, "b"=>2}' + end + end + + describe 'when a symbol is given' do + let(:input) { :hanami } + + it 'returns the string representation' do + expect(@result).to eq 'hanami' + end + end + + describe 'when an struct is given' do + let(:input) { Book.new('DDD') } + + it 'returns the string representation' do + expect(@result).to eq '#' + end + end + + describe 'when an open struct is given' do + let(:input) { OpenStruct.new(title: 'DDD') } + + it 'returns the string representation' do + expect(@result).to eq '#' + end + end + + describe 'when a date is given' do + let(:input) { Date.parse('2014-04-11') } + + it 'returns the string representation' do + expect(@result).to eq '2014-04-11' + end + end + + describe 'when a datetime is given' do + let(:input) { DateTime.parse('2014-04-11 09:45') } + + it 'returns the string representation' do + expect(@result).to eq '2014-04-11T09:45:00+00:00' + end + end + + describe 'when a time is given' do + let(:input) { Time.at(0).utc } + + it 'returns the string representation' do + expect(@result).to eq '1970-01-01 00:00:00 UTC' + end + end + + describe 'when a class is given' do + let(:input) { Fixnum } # rubocop:disable Lint/UnifiedInteger + + if RUBY_VERSION == '2.4.0' + it 'returns the string representation' do + expect(@result).to eq 'Integer' + end + else + it 'returns the string representation' do + expect(@result).to eq 'Fixnum' + end + end + end + + describe 'when a module is given' do + let(:input) { Hanami } + + it 'returns the string representation' do + expect(@result).to eq 'Hanami' + end + end + + describe 'when an object implements #to_s' do + let(:input) { SimpleObject.new } + + it 'returns the string representation' do + expect(@result).to eq 'simple object' + end + end + + describe 'when an object implements #to_str' do + let(:input) { Isbn.new(123) } + + it 'returns the string representation' do + expect(@result).to eq '123' + end + end + end + + describe 'failure operations' do + describe "when a an object that doesn't implement a string interface" do + let(:input) { BasicObject.new } + + it 'raises error' do + expect { Hanami::Utils::Kernel.String(input) }.to raise_error(TypeError) + end + end + end + end + + describe '.Boolean' do + before do + Answer = Struct.new(:answer) do + def to_bool + case answer + when 'yes' then true + else false + end + end + end + end + + after do + Object.send(:remove_const, :Answer) + end + + it 'defines Boolean' do + expect(defined?(::Boolean)).to be_truthy, 'expected class Boolean' + end + + describe 'successful operations' do + before do + @result = Hanami::Utils::Kernel.Boolean(input) + end + + describe 'when nil is given' do + let(:input) { nil } + + it 'returns false' do + expect(@result).to eq false + end + end + + describe 'when true is given' do + let(:input) { true } + + it 'returns true' do + expect(@result).to eq true + end + end + + describe 'when false is given' do + let(:input) { false } + + it 'returns false' do + expect(@result).to eq false + end + end + + describe 'when 0 is given' do + let(:input) { 0 } + + it 'returns false' do + expect(@result).to eq false + end + end + + describe 'when 1 is given' do + let(:input) { 1 } + + it 'returns true' do + expect(@result).to eq true + end + end + + describe 'when 2 is given' do + let(:input) { 2 } + + it 'returns false' do + expect(@result).to eq false + end + end + + describe 'when -1 is given' do + let(:input) { -1 } + + it 'returns false' do + expect(@result).to eq false + end + end + + describe 'when "0" is given (String)' do + let(:input) { '0' } + + it 'returns false' do + expect(@result).to eq false + end + end + + describe 'when "1" is given (String)' do + let(:input) { '1' } + + it 'returns true' do + expect(@result).to eq true + end + end + + describe 'when "foo" is given (String)' do + let(:input) { 'foo' } + + it 'returns false' do + expect(@result).to eq false + end + end + + describe 'when "0" is given (Hanami::Utils::String)' do + let(:input) { Hanami::Utils::String.new('0') } + + it 'returns false' do + expect(@result).to eq false + end + end + + describe 'when "1" is given (Hanami::Utils::String)' do + let(:input) { Hanami::Utils::String.new('1') } + + it 'returns true' do + expect(@result).to eq true + end + end + + describe 'when "foo" is given (Hanami::Utils::String)' do + let(:input) { Hanami::Utils::String.new('foo') } + + it 'returns false' do + expect(@result).to eq false + end + end + + describe 'when an object is given' do + let(:input) { Object.new } + + it 'returns true' do + expect(@result).to eq true + end + end + + describe 'when the given object responds to #to_bool' do + let(:input) { Answer.new('no') } + + it 'returns the result of the serialization' do + expect(@result).to eq false + end + end + end + + describe 'failure operations' do + describe "when a an object that doesn't implement #respond_to?" do + let(:input) { BasicObject.new } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Boolean(input) }.to raise_error(TypeError) + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Boolean(input) + rescue => e + expect(e.message).to eq "can't convert into Boolean" + end + end + end + end + end + + describe '.Date' do + before do + class Christmas + def to_date + Date.parse('Dec, 25') + end + end + + BaseObject = Class.new(BasicObject) do + def respond_to?(_method_name) + false + end + end + end + + after do + Object.send(:remove_const, :Christmas) + Object.send(:remove_const, :BaseObject) + end + + describe 'successful operations' do + before do + @result = Hanami::Utils::Kernel.Date(input) + end + + describe 'when a date is given' do + let(:input) { Date.today } + + it 'returns the input' do + expect(@result).to eq input + end + end + + describe 'when a string that represents a date is given' do + let(:input) { '2014-04-17' } + + it 'returns a date' do + expect(@result).to eq Date.parse(input) + end + end + + describe 'when a string that represents a timestamp is given' do + let(:input) { '2014-04-17 18:50:01' } + + it 'returns a date' do + expect(@result).to eq Date.parse('2014-04-17') + end + end + + describe 'when a time is given' do + let(:input) { Time.now } + + it 'returns a date' do + expect(@result).to eq Date.today + end + end + + describe 'when a datetime is given' do + let(:input) { DateTime.now } + + it 'returns a date' do + expect(@result).to eq Date.today + end + end + + describe 'when an object that implements #to_date is given' do + let(:input) { Christmas.new } + + it 'returns a date' do + expect(@result).to eq Date.parse('Dec, 25') + end + end + end + + describe 'failure operations' do + describe 'when nil is given' do + let(:input) { nil } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Date(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Date(input) + rescue => e + expect(e.message).to eq "can't convert #{input.inspect} into Date" + end + end + end + + describe 'when true is given' do + let(:input) { true } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Date(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Date(input) + rescue => e + expect(e.message).to eq "can't convert #{input.inspect} into Date" + end + end + end + + describe 'when false is given' do + let(:input) { false } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Date(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Date(input) + rescue => e + expect(e.message).to eq "can't convert #{input.inspect} into Date" + end + end + end + + describe 'when a fixnum is given' do + let(:input) { 2 } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Date(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Date(input) + rescue => e + expect(e.message).to eq "can't convert #{input.inspect} into Date" + end + end + end + + describe 'when a float is given' do + let(:input) { 2332.903007 } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Date(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Date(input) + rescue => e + expect(e.message).to eq "can't convert #{input.inspect} into Date" + end + end + end + + describe "when a string that doesn't represent a date is given" do + let(:input) { 'lego' } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Date(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Date(input) + rescue => e + expect(e.message).to eq "can't convert #{input.inspect} into Date" + end + end + end + + describe 'when a string that represent a hour is given' do + let(:input) { '18:55' } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Date(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Date(input) + rescue => e + expect(e.message).to eq "can't convert #{input.inspect} into Date" + end + end + end + + describe "when an object that doesn't implement #respond_to?" do + let(:input) { BasicObject.new } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Date(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Date(input) + rescue => e + expect(e.message).to eq "can't convert into Date" + end + end + end + + describe "when an object that doesn't implement #to_s?" do + let(:input) { BaseObject.new } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Date(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Date(input) + rescue => e + expect(e.message).to eq "can't convert into Date" + end + end + end + end + end + + describe '.DateTime' do + before do + class NewYearEve + def to_datetime + DateTime.parse('Jan, 1') + end + end + + BaseObject = Class.new(BasicObject) do + def respond_to?(_method_name) + false + end + end + end + + after do + Object.send(:remove_const, :NewYearEve) + Object.send(:remove_const, :BaseObject) + end + + describe 'successful operations' do + before do + @result = Hanami::Utils::Kernel.DateTime(input) + end + + describe 'when a datetime is given' do + let(:input) { DateTime.now } + + it 'returns the input' do + expect(@result).to eq input + end + end + + describe 'when a date is given' do + let(:input) { Date.today } + + it 'returns a datetime' do + expect(@result).to eq DateTime.parse(Date.today.to_s) + end + end + + describe 'when a string that represents a date is given' do + let(:input) { '2014-04-17' } + + it 'returns a datetime' do + expect(@result).to eq DateTime.parse(input) + end + end + + describe 'when a string that represents a timestamp is given' do + let(:input) { '2014-04-17 22:51:48' } + + it 'returns a datetime' do + expect(@result).to eq DateTime.parse(input) + end + end + + describe 'when a time is given' do + let(:input) { Time.now } + + it 'returns a datetime' do + expect(@result).to eq input.to_datetime + end + end + + describe 'when an object that implements #to_datetime is given' do + let(:input) { NewYearEve.new } + + it 'returns a datetime' do + expect(@result).to eq DateTime.parse('Jan 1') + end + end + + describe 'when a string that represent a hour is given' do + let(:input) { '23:12' } + + it 'returns a datetime' do + expect(@result).to eq DateTime.parse(input) + end + end + + describe 'when a float is given' do + let(:input) { 2332.903007 } + + it 'raises error' do + expect(@result).to eq Time.at(input).to_datetime + end + end + + describe 'when a fixnum is given' do + let(:input) { 34_322 } + + it 'raises error' do + expect(@result).to eq Time.at(input).to_datetime + end + end + end + + describe 'failure operations' do + describe 'when nil is given' do + let(:input) { nil } + + it 'raises error' do + expect { Hanami::Utils::Kernel.DateTime(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.DateTime(input) + rescue => e + expect(e.message).to eq "can't convert #{input.inspect} into DateTime" + end + end + end + + describe 'when true is given' do + let(:input) { true } + + it 'raises error' do + expect { Hanami::Utils::Kernel.DateTime(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.DateTime(input) + rescue => e + expect(e.message).to eq "can't convert #{input.inspect} into DateTime" + end + end + end + + describe 'when false is given' do + let(:input) { false } + + it 'raises error' do + expect { Hanami::Utils::Kernel.DateTime(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.DateTime(input) + rescue => e + expect(e.message).to eq "can't convert #{input.inspect} into DateTime" + end + end + end + + describe "when a string that doesn't represent a date is given" do + let(:input) { 'crab' } + + it 'raises error' do + expect { Hanami::Utils::Kernel.DateTime(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.DateTime(input) + rescue => e + expect(e.message).to eq "can't convert #{input.inspect} into DateTime" + end + end + end + + describe "when an object that doesn't implement #respond_to?" do + let(:input) { BasicObject.new } + + it 'raises error' do + expect { Hanami::Utils::Kernel.DateTime(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.DateTime(input) + rescue => e + expect(e.message).to eq "can't convert into DateTime" + end + end + end + + describe "when an object that doesn't implement #to_s?" do + let(:input) { BaseObject.new } + + it 'raises error' do + expect { Hanami::Utils::Kernel.DateTime(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.DateTime(input) + rescue => e + expect(e.message).to eq "can't convert into DateTime" + end + end + end + end + end + + describe '.Time' do + before do + class Epoch + def to_time + Time.at(0) + end + end + + BaseObject = Class.new(BasicObject) do + def respond_to?(_method_name) + false + end + end + end + + after do + Object.send(:remove_const, :Epoch) + Object.send(:remove_const, :BaseObject) + end + + describe 'successful operations' do + before do + @result = Hanami::Utils::Kernel.Time(input) + end + + describe 'when a time is given' do + let(:input) { Time.now } + + it 'returns the input' do + expect(@result).to eq input + end + end + + describe 'when a datetime is given' do + let(:input) { DateTime.now } + + it 'returns time' do + expect(@result).to eq input.to_time + end + end + + describe 'when a date is given' do + let(:input) { Date.today } + + it 'returns time' do + expect(@result).to eq input.to_time + end + end + + describe 'when a string that represents a date is given' do + let(:input) { '2014-04-18' } + + it 'returns time' do + expect(@result).to eq Time.parse(input) + end + end + + describe 'when a string that represents a timestamp is given' do + let(:input) { '2014-04-18 15:45:12' } + + it 'returns time' do + expect(@result).to eq Time.parse(input) + end + end + + describe 'when an object that implements #to_time is given' do + let(:input) { Epoch.new } + + it 'returns time' do + expect(@result).to eq Time.at(0) + end + end + + describe 'when a string that represent a hour is given' do + let(:input) { '15:47' } + + it 'returns a time' do + expect(@result).to eq Time.parse(input) + end + end + + describe 'when a fixnum is given' do + let(:input) { 38_922 } + + it 'returns a time' do + expect(@result).to eq Time.at(input) + end + end + + describe 'when a float is given' do + let(:input) { 1332.9423843 } + + it 'returns a time' do + expect(@result).to eq Time.at(input) + end + end + end + + describe 'failure operations' do + describe 'when nil is given' do + let(:input) { nil } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Time(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Time(input) + rescue => e + expect(e.message).to eq "can't convert #{input.inspect} into Time" + end + end + end + + describe 'when true is given' do + let(:input) { true } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Time(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Time(input) + rescue => e + expect(e.message).to eq "can't convert #{input.inspect} into Time" + end + end + end + + describe 'when false is given' do + let(:input) { false } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Time(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Time(input) + rescue => e + expect(e.message).to eq "can't convert #{input.inspect} into Time" + end + end + end + + describe "when a string that doesn't represent a date is given" do + let(:input) { 'boat' } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Time(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Time(input) + rescue => e + expect(e.message).to eq "can't convert #{input.inspect} into Time" + end + end + end + + describe "when an object that doesn't implement #respond_to?" do + let(:input) { BasicObject.new } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Time(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Time(input) + rescue => e + expect(e.message).to eq "can't convert into Time" + end + end + end + + describe "when an object that doesn't implement #to_s" do + let(:input) { BaseObject.new } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Time(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Time(input) + rescue => e + expect(e.message).to eq "can't convert into Time" + end + end + end + end + end + + describe '.Pathname' do + describe 'successful operations' do + before do + class RootPath + def to_str + '/' + end + end + + class HomePath + def to_pathname + Pathname.new Dir.home + end + end + + @result = Hanami::Utils::Kernel.Pathname(input) + end + + after do + Object.send(:remove_const, :RootPath) + Object.send(:remove_const, :HomePath) + end + + describe 'when a pathname is given' do + let(:input) { Pathname.new('.') } + + it 'returns the input' do + expect(@result).to eq input + end + end + + describe 'when a string is given' do + let(:input) { '..' } + + it 'returns a pathname' do + expect(@result).to eq Pathname.new(input) + end + end + + describe 'when an object that implements to_pathname is given' do + let(:input) { HomePath.new } + + it 'returns a pathname' do + expect(@result).to eq Pathname.new(Dir.home) + end + end + + describe 'when an object that implements to_str is given' do + let(:input) { RootPath.new } + + it 'returns a pathname' do + expect(@result).to eq Pathname.new('/') + end + end + end + + describe 'failure operations' do + describe 'when nil is given' do + let(:input) { nil } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Pathname(input) }.to raise_error TypeError + end + end + + describe 'when true is given' do + let(:input) { true } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Pathname(input) }.to raise_error TypeError + end + end + + describe 'when false is given' do + let(:input) { false } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Pathname(input) }.to raise_error TypeError + end + end + + describe 'when a number is given' do + let(:input) { 12 } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Pathname(input) }.to raise_error TypeError + end + end + + describe 'when a date is given' do + let(:input) { Date.today } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Pathname(input) }.to raise_error TypeError + end + end + + describe 'when a datetime is given' do + let(:input) { DateTime.now } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Pathname(input) }.to raise_error TypeError + end + end + + describe 'when a time is given' do + let(:input) { Time.now } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Pathname(input) }.to raise_error TypeError + end + end + + describe "when an object that doesn't implement #respond_to?" do + let(:input) { BasicObject.new } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Pathname(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Pathname(input) + rescue => e + expect(e.message).to eq "can't convert into Pathname" + end + end + end + end + end + + describe '.Symbol' do + describe 'successful operations' do + before do + class StatusSymbol + def to_sym + :success + end + end + + @result = Hanami::Utils::Kernel.Symbol(input) + end + + after do + Object.send(:remove_const, :StatusSymbol) + end + + describe 'when a symbol is given' do + let(:input) { :hello } + + it 'returns a symbol' do + expect(@result).to eq :hello + end + end + + describe 'when a string is given' do + let(:input) { 'hello' } + + it 'returns a symbol' do + expect(@result).to eq :hello + end + end + + describe 'when an object that implements #to_sym' do + let(:input) { StatusSymbol.new } + + it 'returns a symbol' do + expect(@result).to eq :success + end + end + end + + describe 'failure operations' do + describe 'when nil is given' do + let(:input) { nil } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Symbol(input) }.to raise_error TypeError + end + end + + describe 'when empty string is given' do + let(:input) { '' } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Symbol(input) }.to raise_error TypeError + end + end + + describe 'when true is given' do + let(:input) { true } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Symbol(input) }.to raise_error TypeError + end + end + + describe 'when false is given' do + let(:input) { false } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Symbol(input) }.to raise_error TypeError + end + end + + describe 'when a number is given' do + let(:input) { 12 } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Symbol(input) }.to raise_error TypeError + end + end + + describe 'when a date is given' do + let(:input) { Date.today } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Symbol(input) }.to raise_error TypeError + end + end + + describe 'when a datetime is given' do + let(:input) { DateTime.now } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Symbol(input) }.to raise_error TypeError + end + end + + describe 'when a time is given' do + let(:input) { Time.now } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Symbol(input) }.to raise_error TypeError + end + end + + describe "when an object that doesn't implement #respond_to?" do + let(:input) { BasicObject.new } + + it 'raises error' do + expect { Hanami::Utils::Kernel.Symbol(input) }.to raise_error TypeError + end + + it 'returns useful informations about the failure' do + begin + Hanami::Utils::Kernel.Symbol(input) + rescue => e + expect(e.message).to eq "can't convert into Symbol" + end + end + end + end + end + + describe '.numeric?' do + describe 'successful operations' do + before do + @result = Hanami::Utils::Kernel.numeric?(input) + end + + describe 'when a numeric in symbol is given' do + let(:input) { :'123' } + + it 'returns a true' do + expect(@result).to eq true + end + end + + describe 'when a symbol is given' do + let(:input) { :hello } + + it 'returns false' do + expect(@result).to eq false + end + end + + describe 'when a numeric in string is given' do + let(:input) { '123' } + + it 'returns a symbol' do + expect(@result).to eq true + end + end + + describe 'when a string is given' do + let(:input) { 'hello' } + + it 'returns a symbol' do + expect(@result).to eq false + end + end + + describe 'when a numeric is given' do + let(:input) { 123 } + + it 'returns a symbol' do + expect(@result).to eq true + end + end + end + end +end diff --git a/spec/hanami/utils/load_paths_spec.rb b/spec/hanami/utils/load_paths_spec.rb new file mode 100644 index 0000000..b134380 --- /dev/null +++ b/spec/hanami/utils/load_paths_spec.rb @@ -0,0 +1,216 @@ +require 'hanami/utils/load_paths' + +Hanami::Utils::LoadPaths.class_eval do + def empty? + @paths.empty? + end + + def include?(object) + @paths.include?(object) + end +end + +RSpec.describe Hanami::Utils::LoadPaths do + describe '#initialize' do + it 'can be initialized with zero paths' do + paths = Hanami::Utils::LoadPaths.new + expect(paths).to be_empty + end + + it 'can be initialized with one path' do + paths = Hanami::Utils::LoadPaths.new '..' + expect(paths).to include('..') + end + + it 'can be initialized with more paths' do + paths = Hanami::Utils::LoadPaths.new '..', '../..' + + expect(paths).to include('..') + expect(paths).to include('../..') + end + end + + describe '#each' do + it 'coerces the given paths to pathnames and yields a block' do + paths = Hanami::Utils::LoadPaths.new '..', '../..' + + paths.each do |path| + expect(path).to be_kind_of Pathname + end + end + + it 'remove duplicates' do + paths = Hanami::Utils::LoadPaths.new '..', '..' + expect(paths.each(&proc {}).size).to eq 1 + end + + it 'raises an error if a path is unknown' do + paths = Hanami::Utils::LoadPaths.new 'unknown/path' + + expect { paths.each {} }.to raise_error(Errno::ENOENT) + end + end + + describe '#push' do + it 'adds the given path' do + paths = Hanami::Utils::LoadPaths.new '.' + paths.push '..' + + expect(paths).to include '.' + expect(paths).to include '..' + end + + it 'adds the given paths' do + paths = Hanami::Utils::LoadPaths.new '.' + paths.push '..', '../..' + + expect(paths).to include '.' + expect(paths).to include '..' + expect(paths).to include '../..' + end + + it 'removes duplicates' do + paths = Hanami::Utils::LoadPaths.new '.' + paths.push '.', '.' + expect(paths.each(&proc {}).size).to eq 1 + end + + it 'removes nil' do + paths = Hanami::Utils::LoadPaths.new '.' + paths.push nil + expect(paths.each(&proc {}).size).to eq 1 + end + + it 'returns self so multiple operations can be performed' do + paths = Hanami::Utils::LoadPaths.new + + returning = paths.push('.') + expect(returning).to equal(paths) + + paths.push('..').push('../..') + + expect(paths).to include '.' + expect(paths).to include '..' + expect(paths).to include '../..' + end + end + + describe '#<< (alias of #push)' do + it 'adds the given path' do + paths = Hanami::Utils::LoadPaths.new '.' + paths << '..' + + expect(paths).to include '.' + expect(paths).to include '..' + end + + it 'adds the given paths' do + paths = Hanami::Utils::LoadPaths.new '.' + paths << ['..', '../..'] + + expect(paths == ['.', '..', '../..']).to be_truthy + end + + it 'returns self so multiple operations can be performed' do + paths = Hanami::Utils::LoadPaths.new + + returning = paths << '.' + expect(returning).to equal(paths) + + paths << '..' << '../..' + + expect(paths).to include '.' + expect(paths).to include '..' + expect(paths).to include '../..' + end + end + + describe '#dup' do + it 'returns a copy of self' do + paths = Hanami::Utils::LoadPaths.new '.' + paths2 = paths.dup + + paths << '..' + paths2 << '../..' + + expect(paths).to include '.' + expect(paths).to include '..' + expect(paths).not_to include '../..' + + expect(paths).to include '.' + expect(paths2).to include '../..' + expect(paths2).not_to include '..' + end + end + + describe '#clone' do + it 'returns a copy of self' do + paths = Hanami::Utils::LoadPaths.new '.' + paths2 = paths.clone + + paths << '..' + paths2 << '../..' + + expect(paths).to include '.' + expect(paths).to include '..' + expect(paths).not_to include '../..' + + expect(paths).to include '.' + expect(paths2).to include '../..' + expect(paths2).not_to include '..' + end + end + + describe '#freeze' do + it 'freezes the object' do + paths = Hanami::Utils::LoadPaths.new + paths.freeze + + expect(paths).to be_frozen + end + + it "doesn't allow to push paths" do + paths = Hanami::Utils::LoadPaths.new + paths.freeze + + expect { paths.push '.' }.to raise_error(RuntimeError) + end + end + + describe '#==' do + it 'checks equality with LoadPaths' do + paths = Hanami::Utils::LoadPaths.new('.', '.') + other = Hanami::Utils::LoadPaths.new('.') + + expect(paths == other).to be_truthy + end + + it "it returns false if the paths aren't equal" do + paths = Hanami::Utils::LoadPaths.new('.', '..') + other = Hanami::Utils::LoadPaths.new('.') + + expect(paths == other).to be_falsey + end + + it 'checks equality with Array' do + paths = Hanami::Utils::LoadPaths.new('.', '.') + other = ['.'] + + expect(paths == other).to be_truthy + end + + it "it returns false if given array isn't equal" do + paths = Hanami::Utils::LoadPaths.new('.', '..') + other = ['.'] + + expect(paths == other).to be_falsey + end + + it "it returns false the type isn't matchable" do + paths = Hanami::Utils::LoadPaths.new('.', '..') + other = nil + + expect(paths == other).to be_falsey + end + end +end diff --git a/spec/hanami/utils/path_prefix_spec.rb b/spec/hanami/utils/path_prefix_spec.rb new file mode 100644 index 0000000..9ed1a52 --- /dev/null +++ b/spec/hanami/utils/path_prefix_spec.rb @@ -0,0 +1,188 @@ +require 'hanami/utils/path_prefix' + +RSpec.describe Hanami::Utils::PathPrefix do + it 'exposes itself as a string' do + prefix = Hanami::Utils::PathPrefix.new + expect(prefix).to eq('') + end + + it 'adds root prefix only when needed' do + prefix = Hanami::Utils::PathPrefix.new('/fruits') + expect(prefix).to eq('/fruits') + end + + describe '#join' do + it 'returns a PathPrefix' do + prefix = Hanami::Utils::PathPrefix.new('orders', '?').join('new') + expect(prefix).to be_kind_of(Hanami::Utils::PathPrefix) + expect(prefix.__send__(:separator)).to eq('?') + end + + it 'joins a string' do + prefix = Hanami::Utils::PathPrefix.new('fruits') + expect(prefix.join('peaches')).to eq '/fruits/peaches' + end + + it 'joins a prefixed string' do + prefix = Hanami::Utils::PathPrefix.new('fruits') + expect(prefix.join('/cherries')).to eq '/fruits/cherries' + end + + it 'joins a string that is the same as the prefix' do + prefix = Hanami::Utils::PathPrefix.new('fruits') + expect(prefix.join('fruits')).to eq '/fruits/fruits' + end + + it 'joins a string when the root is blank' do + prefix = Hanami::Utils::PathPrefix.new + expect(prefix.join('tea')).to eq '/tea' + end + + it 'joins a prefixed string when the root is blank' do + prefix = Hanami::Utils::PathPrefix.new + expect(prefix.join('/tea')).to eq '/tea' + end + + it 'joins multiple strings' do + prefix = Hanami::Utils::PathPrefix.new + expect(prefix.join('assets', 'application.js')).to eq '/assets/application.js' + + prefix = Hanami::Utils::PathPrefix.new('myapp') + expect(prefix.join('assets', 'application.js')).to eq '/myapp/assets/application.js' + + prefix = Hanami::Utils::PathPrefix.new('/myapp') + expect(prefix.join('/assets', 'application.js')).to eq '/myapp/assets/application.js' + end + + it 'rejects entries that are matching separator' do + prefix = Hanami::Utils::PathPrefix.new('/assets') + expect(prefix.join('/')).to eq '/assets' + end + + it 'removes trailing occurrences of separator' do + prefix = Hanami::Utils::PathPrefix.new('curcuma') + expect(prefix.join(nil)).to eq '/curcuma' + end + end + + describe '#relative_join' do + it 'returns a PathPrefix' do + prefix = Hanami::Utils::PathPrefix.new('orders', '&').relative_join('new') + expect(prefix).to be_kind_of(Hanami::Utils::PathPrefix) + expect(prefix.__send__(:separator)).to eq('&') + end + + it 'joins a string without prefixing with separator' do + prefix = Hanami::Utils::PathPrefix.new('fruits') + expect(prefix.relative_join('peaches')).to eq 'fruits/peaches' + end + + it 'joins a prefixed string without prefixing with separator' do + prefix = Hanami::Utils::PathPrefix.new('fruits') + expect(prefix.relative_join('/cherries')).to eq 'fruits/cherries' + end + + it 'joins a string when the root is blank without prefixing with separator' do + prefix = Hanami::Utils::PathPrefix.new + expect(prefix.relative_join('tea')).to eq 'tea' + end + + it 'joins a prefixed string when the root is blank and removes the prefix' do + prefix = Hanami::Utils::PathPrefix.new + expect(prefix.relative_join('/tea')).to eq 'tea' + end + + it 'joins a string with custom separator' do + prefix = Hanami::Utils::PathPrefix.new('fruits') + expect(prefix.relative_join('cherries', '_')).to eq 'fruits_cherries' + end + + it 'joins a prefixed string without prefixing with custom separator' do + prefix = Hanami::Utils::PathPrefix.new('fruits') + expect(prefix.relative_join('_cherries', '_')).to eq 'fruits_cherries' + end + + it 'changes all the occurences of the current separator with the given one' do + prefix = Hanami::Utils::PathPrefix.new('?fruits', '?') + expect(prefix.relative_join('cherries', '_')).to eq 'fruits_cherries' + end + + it 'removes trailing occurrences of separator' do + prefix = Hanami::Utils::PathPrefix.new('jojoba') + expect(prefix.relative_join(nil)).to eq 'jojoba' + end + + it 'rejects entries that are matching separator' do + prefix = Hanami::Utils::PathPrefix.new('assets') + expect(prefix.relative_join('/')).to eq 'assets' + end + + it 'raises error if the given separator is nil' do + prefix = Hanami::Utils::PathPrefix.new('fruits') + expect { prefix.relative_join('_cherries', nil) }.to raise_error(TypeError) + end + end + + describe 'string interface' do + describe 'equality' do + it 'has a working equality' do + string = Hanami::Utils::PathPrefix.new('hanami') + other = Hanami::Utils::PathPrefix.new('hanami') + + expect(string).to eq(other) + end + + it 'has a working equality with raw strings' do + string = Hanami::Utils::PathPrefix.new('hanami') + expect(string).to eq('hanami') + end + end + + describe 'case equality' do + it 'has a working case equality' do + string = Hanami::Utils::PathPrefix.new('hanami') + other = Hanami::Utils::PathPrefix.new('hanami') + expect(string === other).to be_truthy # rubocop:disable Style/CaseEquality + end + + it 'has a working case equality with raw strings' do + string = Hanami::Utils::PathPrefix.new('hanami') + expect(string === 'hanami').to be_truthy # rubocop:disable Style/CaseEquality + end + end + + describe 'value equality' do + it 'has a working value equality' do + string = Hanami::Utils::PathPrefix.new('hanami') + other = Hanami::Utils::PathPrefix.new('hanami') + expect(string).to eql(other) + end + + it 'has a working value equality with raw strings' do + string = Hanami::Utils::PathPrefix.new('hanami') + expect(string).to eql('hanami') + end + end + + describe 'identity equality' do + it 'has a working identity equality' do + string = Hanami::Utils::PathPrefix.new('hanami') + expect(string).to equal(string) + end + + it 'has a working identity equality with raw strings' do + string = Hanami::Utils::PathPrefix.new('hanami') + expect(string).not_to equal('hanami') + end + end + + describe '#hash' do + it 'returns the same hash result of ::String' do + expected = 'hello'.hash + actual = Hanami::Utils::PathPrefix.new('hello').hash + + expect(actual).to eq expected + end + end + end +end diff --git a/spec/hanami/utils/string_spec.rb b/spec/hanami/utils/string_spec.rb new file mode 100644 index 0000000..8138b85 --- /dev/null +++ b/spec/hanami/utils/string_spec.rb @@ -0,0 +1,423 @@ +require 'hanami/utils/string' + +RSpec.describe Hanami::Utils::String do + describe '#titleize' do + it 'returns an instance of Hanami::Utils::String' do + expect(Hanami::Utils::String.new('hanami').titleize).to be_kind_of(Hanami::Utils::String) + end + + it 'does not mutate self' do + string = Hanami::Utils::String.new('hanami') + string.titleize + expect(string).to eq('hanami') + end + + it 'returns an titleized string' do + expect(Hanami::Utils::String.new('hanami').titleize).to eq('Hanami') + expect(Hanami::Utils::String.new('HanamiUtils').titleize).to eq('Hanami Utils') + expect(Hanami::Utils::String.new('hanami utils').titleize).to eq('Hanami Utils') + expect(Hanami::Utils::String.new('hanami_utils').titleize).to eq('Hanami Utils') + expect(Hanami::Utils::String.new('hanami-utils').titleize).to eq('Hanami Utils') + expect(Hanami::Utils::String.new("hanami' utils").titleize).to eq("Hanami' Utils") + expect(Hanami::Utils::String.new('hanami’ utils').titleize).to eq('Hanami’ Utils') + expect(Hanami::Utils::String.new('hanami` utils').titleize).to eq('Hanami` Utils') + end + end + + describe '#capitalize' do + it 'returns an instance of Hanami::Utils::String' do + expect(Hanami::Utils::String.new('hanami').capitalize).to be_kind_of(Hanami::Utils::String) + end + + it "doesn't mutate self" do + string = Hanami::Utils::String.new('hanami') + string.capitalize + expect(string).to eq('hanami') + end + + it 'returns an capitalized string' do + expect(Hanami::Utils::String.new('hanami').capitalize).to eq('Hanami') + expect(Hanami::Utils::String.new('HanamiUtils').capitalize).to eq('Hanami utils') + expect(Hanami::Utils::String.new('hanami utils').capitalize).to eq('Hanami utils') + expect(Hanami::Utils::String.new('hanami_utils').capitalize).to eq('Hanami utils') + expect(Hanami::Utils::String.new('hanami-utils').capitalize).to eq('Hanami utils') + expect(Hanami::Utils::String.new("hanami' utils").capitalize).to eq("Hanami' utils") + expect(Hanami::Utils::String.new('hanami’ utils').capitalize).to eq('Hanami’ utils') + expect(Hanami::Utils::String.new('hanami` utils').capitalize).to eq('Hanami` utils') + expect(Hanami::Utils::String.new('OneTwoThree').capitalize).to eq('One two three') + expect(Hanami::Utils::String.new('one Two three').capitalize).to eq('One two three') + expect(Hanami::Utils::String.new('one_two_three').capitalize).to eq('One two three') + expect(Hanami::Utils::String.new('one-two-three').capitalize).to eq('One two three') + + expect(Hanami::Utils::String.new(:HanamiUtils).capitalize).to eq('Hanami utils') + expect(Hanami::Utils::String.new(:'hanami utils').capitalize).to eq('Hanami utils') + expect(Hanami::Utils::String.new(:hanami_utils).capitalize).to eq('Hanami utils') + expect(Hanami::Utils::String.new(:'hanami-utils').capitalize).to eq('Hanami utils') + end + end + + describe '#classify' do + it 'returns an instance of Hanami::Utils::String' do + expect(Hanami::Utils::String.new('hanami').classify).to be_kind_of(Hanami::Utils::String) + end + + it 'returns a classified string' do + expect(Hanami::Utils::String.new('hanami').classify).to eq('Hanami') + expect(Hanami::Utils::String.new('hanami_router').classify).to eq('HanamiRouter') + expect(Hanami::Utils::String.new('hanami-router').classify).to eq('Hanami::Router') + expect(Hanami::Utils::String.new('hanami/router').classify).to eq('Hanami::Router') + expect(Hanami::Utils::String.new('hanami::router').classify).to eq('Hanami::Router') + expect(Hanami::Utils::String.new('hanami::router/base_object').classify).to eq('Hanami::Router::BaseObject') + end + + it 'returns a classified string from symbol' do + expect(Hanami::Utils::String.new(:hanami).classify).to eq('Hanami') + expect(Hanami::Utils::String.new(:hanami_router).classify).to eq('HanamiRouter') + expect(Hanami::Utils::String.new(:'hanami-router').classify).to eq('Hanami::Router') + expect(Hanami::Utils::String.new(:'hanami/router').classify).to eq('Hanami::Router') + expect(Hanami::Utils::String.new(:'hanami::router').classify).to eq('Hanami::Router') + end + + it 'does not remove capital letter in string' do + expect(Hanami::Utils::String.new('AwesomeProject').classify).to eq('AwesomeProject') + end + end + + describe '#underscore' do + it 'returns an instance of Hanami::Utils::String' do + expect(Hanami::Utils::String.new('Hanami').underscore).to be_kind_of(Hanami::Utils::String) + end + + it 'does not mutate itself' do + string = Hanami::Utils::String.new('Hanami') + string.underscore + expect(string).to eq('Hanami') + end + + it 'removes all the upcase characters' do + string = Hanami::Utils::String.new('Hanami') + expect(string.underscore).to eq('hanami') + end + + it 'transforms camel case class names' do + string = Hanami::Utils::String.new('HanamiView') + expect(string.underscore).to eq('hanami_view') + end + + it 'substitutes double colons with path separators' do + string = Hanami::Utils::String.new('Hanami::Utils::String') + expect(string.underscore).to eq('hanami/utils/string') + end + + it 'handles acronyms' do + string = Hanami::Utils::String.new('APIDoc') + expect(string.underscore).to eq('api_doc') + end + + it 'handles numbers' do + string = Hanami::Utils::String.new('Lucky23Action') + expect(string.underscore).to eq('lucky23_action') + end + + it 'handles dashes' do + string = Hanami::Utils::String.new('hanami-utils') + expect(string.underscore).to eq('hanami_utils') + end + + it 'handles spaces' do + string = Hanami::Utils::String.new('Hanami Utils') + expect(string.underscore).to eq('hanami_utils') + end + + it 'handles accented letters' do + string = Hanami::Utils::String.new('è vero') + expect(string.underscore).to eq('è_vero') + end + end + + describe '#dasherize' do + it 'returns an instance of Hanami::Utils::String' do + expect(Hanami::Utils::String.new('Hanami').dasherize).to be_kind_of(Hanami::Utils::String) + end + + it 'does not mutate itself' do + string = Hanami::Utils::String.new('Hanami') + string.dasherize + expect(string).to eq('Hanami') + end + + it 'removes all the upcase characters' do + string = Hanami::Utils::String.new('Hanami') + expect(string.dasherize).to eq('hanami') + end + + it 'transforms camel case class names' do + string = Hanami::Utils::String.new('HanamiView') + expect(string.dasherize).to eq('hanami-view') + end + + it 'handles acronyms' do + string = Hanami::Utils::String.new('APIDoc') + expect(string.dasherize).to eq('api-doc') + end + + it 'handles numbers' do + string = Hanami::Utils::String.new('Lucky23Action') + expect(string.dasherize).to eq('lucky23-action') + end + + it 'handles underscores' do + string = Hanami::Utils::String.new('hanami_utils') + expect(string.dasherize).to eq('hanami-utils') + end + + it 'handles spaces' do + string = Hanami::Utils::String.new('Hanami Utils') + expect(string.dasherize).to eq('hanami-utils') + end + + it 'handles accented letters' do + string = Hanami::Utils::String.new('è vero') + expect(string.dasherize).to eq('è-vero') + end + end + + describe '#demodulize' do + it 'returns an instance of Hanami::Utils::String' do + expect(Hanami::Utils::String.new('Hanami').demodulize).to be_kind_of(Hanami::Utils::String) + end + + it 'returns the class name without the namespace' do + expect(Hanami::Utils::String.new('String').demodulize).to eq('String') + expect(Hanami::Utils::String.new('Hanami::Utils::String').demodulize).to eq('String') + end + end + + describe '#namespace' do + it 'returns an instance of Hanami::Utils::String' do + expect(Hanami::Utils::String.new('Hanami').namespace).to be_kind_of(Hanami::Utils::String) + end + + it 'returns the top level module name' do + expect(Hanami::Utils::String.new('String').namespace).to eq('String') + expect(Hanami::Utils::String.new('Hanami::Utils::String').namespace).to eq('Hanami') + end + end + + describe '#tokenize' do + before do + @logger = [] + end + + it 'returns an instance of Hanami::Utils::String' do + string = Hanami::Utils::String.new('Hanami::(Utils|App)') + string.tokenize do |token| + @logger.push token + end + + expect(@logger).to all(be_kind_of(Hanami::Utils::String)) + end + + it 'calls the given block for each token occurrence' do + string = Hanami::Utils::String.new('Hanami::(Utils|App)') + string.tokenize do |token| + @logger.push token + end + + expect(@logger).to eq(['Hanami::Utils', 'Hanami::App']) + end + + it 'guarantees the block to be called even when the token conditions are not met' do + string = Hanami::Utils::String.new('Hanami') + string.tokenize do |token| + @logger.push token + end + + expect(@logger).to eq(['Hanami']) + end + + it 'returns nil' do + result = Hanami::Utils::String.new('Hanami::(Utils|App)').tokenize {} + expect(result).to be_nil + end + end + + describe '#pluralize' do + before do + @singular, @plural = *TEST_PLURALS.to_a.sample + end + + it 'returns a Hanami::Utils::String instance' do + result = Hanami::Utils::String.new(@singular).pluralize + expect(result).to be_kind_of(Hanami::Utils::String) + end + + it 'pluralizes string' do + result = Hanami::Utils::String.new(@singular).pluralize + expect(result).to eq(@plural) + end + + it 'does not modify the original string' do + string = Hanami::Utils::String.new(@singular) + + expect(string.pluralize).to eq(@plural) + expect(string).to eq(@singular) + end + end + + describe '#singularize' do + before do + @singular, @plural = *TEST_SINGULARS.to_a.sample + end + + it 'returns a Hanami::Utils::String instance' do + result = Hanami::Utils::String.new(@plural).singularize + expect(result).to be_kind_of(Hanami::Utils::String) + end + + it 'singularizes string' do + result = Hanami::Utils::String.new(@plural).singularize + expect(result).to eq(@singular) + end + + it 'does not modify the original string' do + string = Hanami::Utils::String.new(@plural) + + expect(string.singularize).to eq(@singular) + expect(string).to eq(@plural) + end + end + + describe '#rsub' do + it 'returns a Hanami::Utils::String instance' do + result = Hanami::Utils::String.new('authors/books/index').rsub(//, '') + expect(result).to be_kind_of(Hanami::Utils::String) + end + + it 'does not mutate original string' do + string = Hanami::Utils::String.new('authors/books/index') + string.rsub(%r{/}, '#') + + expect(string).to eq('authors/books/index') + end + + it 'replaces rightmost instance (regexp)' do + result = Hanami::Utils::String.new('authors/books/index').rsub(%r{/}, '#') + expect(result).to eq('authors/books#index') + end + + it 'replaces rightmost instance (string)' do + result = Hanami::Utils::String.new('authors/books/index').rsub('/', '#') + expect(result).to eq('authors/books#index') + end + + it 'accepts Hanami::Utils::String as replacement' do + replacement = Hanami::Utils::String.new('#') + result = Hanami::Utils::String.new('authors/books/index').rsub(%r{/}, replacement) + + expect(result).to eq('authors/books#index') + end + + it 'returns the initial string no match' do + result = Hanami::Utils::String.new('index').rsub(%r{/}, '#') + expect(result).to eq('index') + end + end + + describe 'string interface' do + it 'responds to ::String methods and returns a new Hanami::Utils::String' do + string = Hanami::Utils::String.new("Hanami\n").chomp + expect(string).to eq('Hanami') + expect(string).to be_kind_of Hanami::Utils::String + end + + it 'responds to ::String methods and only returns a new Hanami::Utils::String when the return value is a string' do + string = Hanami::Utils::String.new('abcdef') + expect(string.casecmp('abcde')).to eq(1) + end + + it 'responds to whatever ::String responds to' do + string = Hanami::Utils::String.new('abcdef') + + expect(string).to respond_to :reverse + expect(string).not_to respond_to :unknown_method + end + + describe 'equality' do + it 'has a working equality' do + string = Hanami::Utils::String.new('hanami') + other = Hanami::Utils::String.new('hanami') + + expect(string.==(other)).to be_truthy + end + + it 'has a working equality with raw strings' do + string = Hanami::Utils::String.new('hanami') + expect(string.==('hanami')).to be_truthy + end + end + + describe 'case equality' do + it 'has a working case equality' do + string = Hanami::Utils::String.new('hanami') + other = Hanami::Utils::String.new('hanami') + expect(string.===(other)).to be_truthy # rubocop:disable Style/CaseEquality + end + + it 'has a working case equality with raw strings' do + string = Hanami::Utils::String.new('hanami') + expect(string.===('hanami')).to be_truthy # rubocop:disable Style/CaseEquality + end + end + + describe 'value equality' do + it 'has a working value equality' do + string = Hanami::Utils::String.new('hanami') + other = Hanami::Utils::String.new('hanami') + expect(string).to eql(other) + end + + it 'has a working value equality with raw strings' do + string = Hanami::Utils::String.new('hanami') + expect(string).to eql('hanami') + end + end + + describe 'identity equality' do + it 'has a working identity equality' do + string = Hanami::Utils::String.new('hanami') + expect(string).to equal(string) + end + + it 'has a working identity equality with raw strings' do + string = Hanami::Utils::String.new('hanami') + expect(string).not_to equal('hanami') + end + end + + describe '#hash' do + it 'returns the same hash result of ::String' do + expected = 'hello'.hash + actual = Hanami::Utils::String.new('hello').hash + + expect(actual).to eq(expected) + end + end + end + + describe 'unknown method' do + it 'raises error' do + expect { Hanami::Utils::String.new('one').yay! } + .to raise_error(NoMethodError, %(undefined method `yay!' for "one":Hanami::Utils::String)) + end + + # See: https://github.com/hanami/utils/issues/48 + it 'returns the correct object when a NoMethodError is raised' do + string = Hanami::Utils::String.new('/path/to/something') + exception_message = %(undefined method `boom' for "/":String) + + expect { string.gsub(%r{/}, &:boom) } + .to raise_error(NoMethodError, exception_message) + end + end +end diff --git a/spec/hanami/utils/utils_spec.rb b/spec/hanami/utils/utils_spec.rb new file mode 100644 index 0000000..fa96b01 --- /dev/null +++ b/spec/hanami/utils/utils_spec.rb @@ -0,0 +1,21 @@ +RSpec.describe Hanami::Utils do + describe '.jruby?' do + it 'introspects the current platform' do + if RUBY_PLATFORM == 'java' + expect(Hanami::Utils.jruby?).to eq(true) + else + expect(Hanami::Utils.jruby?).to eq(false) + end + end + end + + describe '.rubinius?' do + it 'introspects the current platform' do + if RUBY_ENGINE == 'rbx' + expect(Hanami::Utils.rubinius?).to eq(true) + else + expect(Hanami::Utils.rubinius?).to eq(false) + end + end + end +end diff --git a/spec/hanami/utils/version_spec.rb b/spec/hanami/utils/version_spec.rb new file mode 100644 index 0000000..94b07d1 --- /dev/null +++ b/spec/hanami/utils/version_spec.rb @@ -0,0 +1,5 @@ +RSpec.describe Hanami::Utils::VERSION do + it 'exposes version' do + expect(Hanami::Utils::VERSION).to eq('1.0.0.beta3') + end +end diff --git a/spec/isolation/.rspec b/spec/isolation/.rspec new file mode 100644 index 0000000..4e1e0d2 --- /dev/null +++ b/spec/isolation/.rspec @@ -0,0 +1 @@ +--color diff --git a/spec/isolation/json/json_spec.rb b/spec/isolation/json/json_spec.rb new file mode 100644 index 0000000..daba32f --- /dev/null +++ b/spec/isolation/json/json_spec.rb @@ -0,0 +1,29 @@ +require_relative '../../support/isolation_spec_helper' +require 'hanami/utils/json' + +RSpec.describe Hanami::Utils::Json do + describe 'with JSON' do + it 'uses JSON engine' do + expect(Hanami::Utils::Json.class_variable_get(:@@engine)).to eq(JSON) + end + + describe '.parse' do + it 'loads given payload' do + actual = Hanami::Utils::Json.parse %({"a":1}) + expect(actual).to eq('a' => 1) + end + + it 'raises error if given payload is malformed' do + expect { Hanami::Utils::Json.parse %({"a:1}) }.to raise_error(Hanami::Utils::Json::ParserError) + end + + # See: https://github.com/hanami/utils/issues/169 + it "doesn't eval payload" do + actual = Hanami::Utils::Json.parse %({"json_class": "Foo"}) + expect(actual).to eq('json_class' => 'Foo') + end + end + end +end + +RSpec::Support::Runner.run diff --git a/spec/isolation/json/multi_json_spec.rb b/spec/isolation/json/multi_json_spec.rb new file mode 100644 index 0000000..0c6fb55 --- /dev/null +++ b/spec/isolation/json/multi_json_spec.rb @@ -0,0 +1,40 @@ +require_relative '../../support/isolation_spec_helper' + +Bundler.require(:default, :development, :multi_json) + +require 'gson' if Hanami::Utils.jruby? +require 'hanami/utils/json' + +RSpec.describe Hanami::Utils::Json do + describe 'with MultiJson' do + it 'uses MultiJson engine' do + expect(Hanami::Utils::Json.class_variable_get(:@@engine)).to be_kind_of(Hanami::Utils::Json::MultiJsonAdapter) + end + + describe '.parse' do + it 'loads given payload' do + actual = Hanami::Utils::Json.parse %({"a":1}) + expect(actual).to eq('a' => 1) + end + + it 'raises error if given payload is malformed' do + expect { Hanami::Utils::Json.parse %({"a:1}) }.to raise_error(Hanami::Utils::Json::ParserError) + end + + # See: https://github.com/hanami/utils/issues/169 + it "doesn't eval payload" do + actual = Hanami::Utils::Json.parse %({"json_class": "Foo"}) + expect(actual).to eq('json_class' => 'Foo') + end + end + + describe '.generate' do + it 'dumps given Hash' do + actual = Hanami::Utils::Json.generate(a: 1) + expect(actual).to eq %({"a":1}) + end + end + end +end + +RSpec::Support::Runner.run diff --git a/spec/isolation/reload_spec.rb b/spec/isolation/reload_spec.rb new file mode 100644 index 0000000..51d0adf --- /dev/null +++ b/spec/isolation/reload_spec.rb @@ -0,0 +1,44 @@ +require_relative '../support/isolation_spec_helper' + +RSpec.describe 'Hanami::Utils.reload!' do + before do + FileUtils.rm_rf(root) if root.exist? + root.mkpath + end + + after do + FileUtils.rm_rf(root.parent) + end + + let(:root) { Pathname.new(Dir.pwd).join('tmp', 'reload') } + + it 'reloads the files set of files' do + File.open(root.join('user.rb'), 'w+') do |f| + f.write <<-EOF + class User + def greet + "Hi" + end + end + EOF + end + + Hanami::Utils.reload!(root) + expect(User.new.greet).to eq "Hi" + + File.open(root.join('user.rb'), 'w+') do |f| + f.write <<-EOF + class User + def greet + "Ciao" + end + end + EOF + end + + Hanami::Utils.reload!(root) + expect(User.new.greet).to eq "Ciao" + end +end + +RSpec::Support::Runner.run diff --git a/spec/isolation/require/with_absolute_path_spec.rb b/spec/isolation/require/with_absolute_path_spec.rb new file mode 100644 index 0000000..c69ba46 --- /dev/null +++ b/spec/isolation/require/with_absolute_path_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../support/isolation_spec_helper' + +RSpec.describe 'Hanami::Utils.require!' do + describe 'with absolute path' do + it 'requires ordered set of files' do + directory = Pathname.new(Dir.pwd).join('spec', 'support', 'fixtures', 'file_list') + Hanami::Utils.require!(directory) + + expect(defined?(A)).to be_truthy, 'expected A to be defined' + expect(defined?(Aa)).to be_truthy, 'expected Aa to be defined' + expect(defined?(Ab)).to be_truthy, 'expected Ab to be defined' + expect(defined?(C)).to be_truthy, 'expected C to be defined' + end + end +end + +RSpec::Support::Runner.run diff --git a/spec/isolation/require/with_file_separator_spec.rb b/spec/isolation/require/with_file_separator_spec.rb new file mode 100644 index 0000000..a6a0ed2 --- /dev/null +++ b/spec/isolation/require/with_file_separator_spec.rb @@ -0,0 +1,22 @@ +require_relative '../../support/isolation_spec_helper' + +RSpec.describe 'Hanami::Utils.require!' do + describe 'with file separator' do + it 'applies current operating system file separator' do + # Invert the file separator for the current operating system: + # + # * on *NIX systems, instead of having /, we get \ + # * on Windows systems, instead of having \, we get / + separator = File::SEPARATOR == '/' ? '\\' : '/' + directory = %w(spec support fixtures file_list).join(separator) + Hanami::Utils.require!(directory) + + expect(defined?(A)).to be_truthy, 'expected A to be defined' + expect(defined?(Aa)).to be_truthy, 'expected Aa to be defined' + expect(defined?(Ab)).to be_truthy, 'expected Ab to be defined' + expect(defined?(C)).to be_truthy, 'expected C to be defined' + end + end +end + +RSpec::Support::Runner.run diff --git a/spec/isolation/require/with_recursive_pattern_spec.rb b/spec/isolation/require/with_recursive_pattern_spec.rb new file mode 100644 index 0000000..c2ff02a --- /dev/null +++ b/spec/isolation/require/with_recursive_pattern_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../support/isolation_spec_helper' + +RSpec.describe 'Hanami::Utils.require!' do + describe 'with file separator' do + it 'requires ordered set of files' do + directory = %w(spec support fixtures file_list ** *.rb).join(File::SEPARATOR) + Hanami::Utils.require!(directory) + + expect(defined?(A)).to be_truthy, 'expected A to be defined' + expect(defined?(Aa)).to be_truthy, 'expected Aa to be defined' + expect(defined?(Ab)).to be_truthy, 'expected Ab to be defined' + expect(defined?(C)).to be_truthy, 'expected C to be defined' + end + end +end + +RSpec::Support::Runner.run diff --git a/spec/isolation/require/with_relative_path_spec.rb b/spec/isolation/require/with_relative_path_spec.rb new file mode 100644 index 0000000..70d9413 --- /dev/null +++ b/spec/isolation/require/with_relative_path_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../support/isolation_spec_helper' + +RSpec.describe 'Hanami::Utils.require!' do + describe 'with relative path' do + it 'requires ordered set of files' do + directory = Pathname.new('spec').join('support', 'fixtures', 'file_list') + Hanami::Utils.require!(directory) + + expect(defined?(A)).to be_truthy, 'expected A to be defined' + expect(defined?(Aa)).to be_truthy, 'expected Aa to be defined' + expect(defined?(Ab)).to be_truthy, 'expected Ab to be defined' + expect(defined?(C)).to be_truthy, 'expected C to be defined' + end + end +end + +RSpec::Support::Runner.run diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..5cbcffb --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,9 @@ +if ENV['COVERALL'] + require 'coveralls' + Coveralls.wear! +end + +$LOAD_PATH.unshift 'lib' +require 'hanami/utils' + +Hanami::Utils.require!("spec/support") diff --git a/spec/support/fixtures/file_list/a.rb b/spec/support/fixtures/file_list/a.rb new file mode 100644 index 0000000..eea30e8 --- /dev/null +++ b/spec/support/fixtures/file_list/a.rb @@ -0,0 +1,2 @@ +class A +end diff --git a/spec/support/fixtures/file_list/aa.rb b/spec/support/fixtures/file_list/aa.rb new file mode 100644 index 0000000..e1ede3f --- /dev/null +++ b/spec/support/fixtures/file_list/aa.rb @@ -0,0 +1,2 @@ +class Aa +end diff --git a/spec/support/fixtures/file_list/ab.rb b/spec/support/fixtures/file_list/ab.rb new file mode 100644 index 0000000..e1984f4 --- /dev/null +++ b/spec/support/fixtures/file_list/ab.rb @@ -0,0 +1,2 @@ +class Ab +end diff --git a/spec/support/fixtures/file_list/nested/c.rb b/spec/support/fixtures/file_list/nested/c.rb new file mode 100644 index 0000000..b2955f5 --- /dev/null +++ b/spec/support/fixtures/file_list/nested/c.rb @@ -0,0 +1,2 @@ +class C +end diff --git a/spec/support/fixtures/fixtures.rb b/spec/support/fixtures/fixtures.rb new file mode 100644 index 0000000..697e282 --- /dev/null +++ b/spec/support/fixtures/fixtures.rb @@ -0,0 +1,628 @@ +TEST_REPLACEMENT_CHAR = 'fffd'.freeze +TEST_INVALID_CHARS = ((0x0..0x8).to_a + (0x11..0x12).to_a + (0x14..0x1f).to_a).flatten.each_with_object({}) do |char, result| + char = char.chr(Encoding::UTF_8) + result[char] = TEST_REPLACEMENT_CHAR +end + +TEST_HTML_ENTITIES = { + '"' => 'quot', + '&' => 'amp', + '<' => 'lt', + '>' => 'gt', + ' ' => 'nbsp', + '¡' => 'iexcl', + '¢' => 'cent', + '£' => 'pound', + '¤' => 'curren', + '¥' => 'yen', + '¦' => 'brvbar', + '§' => 'sect', + '¨' => 'uml', + '©' => 'copy', + 'ª' => 'ordf', + '«' => 'laquo', + '¬' => 'not', + '­' => 'shy', + '®' => 'reg', + '¯' => 'macr', + '°' => 'deg', + '±' => 'plusmn', + '²' => 'sup2', + '³' => 'sup3', + '´' => 'acute', + 'µ' => 'micro', + '¶' => 'para', + '·' => 'middot', + '¸' => 'cedil', + '¹' => 'sup1', + 'º' => 'ordm', + '»' => 'raquo', + '¼' => 'frac14', + '½' => 'frac12', + '¾' => 'frac34', + '¿' => 'iquest', + 'À' => 'Agrave', + 'Á' => 'Aacute', + 'Â' => 'Acirc', + 'Ã' => 'Atilde', + 'Ä' => 'Auml', + 'Å' => 'Aring', + 'Æ' => 'AElig', + 'Ç' => 'Ccedil', + 'È' => 'Egrave', + 'É' => 'Eacute', + 'Ê' => 'Ecirc', + 'Ë' => 'Euml', + 'Ì' => 'Igrave', + 'Í' => 'Iacute', + 'Î' => 'Icirc', + 'Ï' => 'Iuml', + 'Ð' => 'ETH', + 'Ñ' => 'Ntilde', + 'Ò' => 'Ograve', + 'Ó' => 'Oacute', + 'Ô' => 'Ocirc', + 'Õ' => 'Otilde', + 'Ö' => 'Ouml', + '×' => 'times', + 'Ø' => 'Oslash', + 'Ù' => 'Ugrave', + 'Ú' => 'Uacute', + 'Û' => 'Ucirc', + 'Ü' => 'Uuml', + 'Ý' => 'Yacute', + 'Þ' => 'THORN', + 'ß' => 'szlig', + 'à' => 'agrave', + 'á' => 'aacute', + 'â' => 'acirc', + 'ã' => 'atilde', + 'ä' => 'auml', + 'å' => 'aring', + 'æ' => 'aelig', + 'ç' => 'ccedil', + 'è' => 'egrave', + 'é' => 'eacute', + 'ê' => 'ecirc', + 'ë' => 'euml', + 'ì' => 'igrave', + 'í' => 'iacute', + 'î' => 'icirc', + 'ï' => 'iuml', + 'ð' => 'eth', + 'ñ' => 'ntilde', + 'ò' => 'ograve', + 'ó' => 'oacute', + 'ô' => 'ocirc', + 'õ' => 'otilde', + 'ö' => 'ouml', + '÷' => 'divide', + 'ø' => 'oslash', + 'ù' => 'ugrave', + 'ú' => 'uacute', + 'û' => 'ucirc', + 'ü' => 'uuml', + 'ý' => 'yacute', + 'þ' => 'thorn', + 'ÿ' => 'yuml', + 'Œ' => 'OElig', + 'œ' => 'oelig', + 'Š' => 'Scaron', + 'š' => 'scaron', + 'Ÿ' => 'Yuml', + 'ƒ' => 'fnof', + 'ˆ' => 'circ', + '˜' => 'tilde', + 'Α' => 'Alpha', + 'Β' => 'Beta', + 'Γ' => 'Gamma', + 'Δ' => 'Delta', + 'Ε' => 'Epsilon', + 'Ζ' => 'Zeta', + 'Η' => 'Eta', + 'Θ' => 'Theta', + 'Ι' => 'Iota', + 'Κ' => 'Kappa', + 'Λ' => 'Lambda', + 'Μ' => 'Mu', + 'Ν' => 'Nu', + 'Ξ' => 'Xi', + 'Ο' => 'Omicron', + 'Π' => 'Pi', + 'Ρ' => 'Rho', + 'Σ' => 'Sigma', + 'Τ' => 'Tau', + 'Υ' => 'Upsilon', + 'Φ' => 'Phi', + 'Χ' => 'Chi', + 'Ψ' => 'Psi', + 'Ω' => 'Omega', + 'α' => 'alpha', + 'β' => 'beta', + 'γ' => 'gamma', + 'δ' => 'delta', + 'ε' => 'epsilon', + 'ζ' => 'zeta', + 'η' => 'eta', + 'θ' => 'theta', + 'ι' => 'iota', + 'κ' => 'kappa', + 'λ' => 'lambda', + 'μ' => 'mu', + 'ν' => 'nu', + 'ξ' => 'xi', + 'ο' => 'omicron', + 'π' => 'pi', + 'ρ' => 'rho', + 'ς' => 'sigmaf', + 'σ' => 'sigma', + 'τ' => 'tau', + 'υ' => 'upsilon', + 'φ' => 'phi', + 'χ' => 'chi', + 'ψ' => 'psi', + 'ω' => 'omega', + 'ϑ' => 'thetasym', + 'ϒ' => 'upsih', + 'ϖ' => 'piv', + "\u2002" => 'ensp', + "\u2003" => 'emsp', + "\u2009" => 'thinsp', + "\u200C" => 'zwnj', + "\u200D" => 'zwj', + "\u200E" => 'lrm', + "\u200F" => 'rlm', + '–' => 'ndash', + '—' => 'mdash', + '‘' => 'lsquo', + '’' => 'rsquo', + '‚' => 'sbquo', + '“' => 'ldquo', + '”' => 'rdquo', + '„' => 'bdquo', + '†' => 'dagger', + '‡' => 'Dagger', + '•' => 'bull', + '…' => 'hellip', + '‰' => 'permil', + '′' => 'prime', + '″' => 'Prime', + '‹' => 'lsaquo', + '›' => 'rsaquo', + '‾' => 'oline', + '⁄' => 'frasl', + '€' => 'euro', + 'ℑ' => 'image', + '℘' => 'weierp', + 'ℜ' => 'real', + '™' => 'trade', + 'ℵ' => 'alefsym', + '←' => 'larr', + '↑' => 'uarr', + '→' => 'rarr', + '↓' => 'darr', + '↔' => 'harr', + '↵' => 'crarr', + '⇐' => 'lArr', + '⇑' => 'uArr', + '⇒' => 'rArr', + '⇓' => 'dArr', + '⇔' => 'hArr', + '∀' => 'forall', + '∂' => 'part', + '∃' => 'exist', + '∅' => 'empty', + '∇' => 'nabla', + '∈' => 'isin', + '∉' => 'notin', + '∋' => 'ni', + '∏' => 'prod', + '∑' => 'sum', + '−' => 'minus', + '∗' => 'lowast', + '√' => 'radic', + '∝' => 'prop', + '∞' => 'infin', + '∠' => 'ang', + '∧' => 'and', + '∨' => 'or', + '∩' => 'cap', + '∪' => 'cup', + '∫' => 'int', + '∴' => 'there4', + '∼' => 'sim', + '≅' => 'cong', + '≈' => 'asymp', + '≠' => 'ne', + '≡' => 'equiv', + '≤' => 'le', + '≥' => 'ge', + '⊂' => 'sub', + '⊃' => 'sup', + '⊄' => 'nsub', + '⊆' => 'sube', + '⊇' => 'supe', + '⊕' => 'oplus', + '⊗' => 'otimes', + '⊥' => 'perp', + '⋅' => 'sdot', + '⌈' => 'lceil', + '⌉' => 'rceil', + '⌊' => 'lfloor', + '⌋' => 'rfloor', + "\u2329" => 'lang', # rubocop:disable Style/AsciiComments "〈" + "\u232A" => 'rang', # rubocop:disable Style/AsciiComments "〉" + '◊' => 'loz', + '♠' => 'spades', + '♣' => 'clubs', + '♥' => 'hearts', + '♦' => 'diams' +}.freeze + +TEST_PLURALS = { + # um => a + 'bacterium' => 'bacteria', + 'agendum' => 'agenda', + 'desideratum' => 'desiderata', + 'erratum' => 'errata', + 'stratum' => 'strata', + 'datum' => 'data', + 'ovum' => 'ova', + 'extremum' => 'extrema', + 'candelabrum' => 'candelabra', + 'curriculum' => 'curricula', + 'millennium' => 'millennia', + 'referendum' => 'referenda', + 'stadium' => 'stadia', + 'medium' => 'media', + 'memorandum' => 'memoranda', + 'criterium' => 'criteria', + 'perihelium' => 'perihelia', + 'aphelium' => 'aphelia', + # on => a + 'phenomenon' => 'phenomena', + 'prolegomenon' => 'prolegomena', + 'noumenon' => 'noumena', + 'organon' => 'organa', + # o => os + 'albino' => 'albinos', + 'archipelago' => 'archipelagos', + 'armadillo' => 'armadillos', + 'commando' => 'commandos', + 'crescendo' => 'crescendos', + 'fiasco' => 'fiascos', + 'ditto' => 'dittos', + 'dynamo' => 'dynamos', + 'embryo' => 'embryos', + 'ghetto' => 'ghettos', + 'guano' => 'guanos', + 'inferno' => 'infernos', + 'jumbo' => 'jumbos', + 'lumbago' => 'lumbagos', + 'magneto' => 'magnetos', + 'manifesto' => 'manifestos', + 'medico' => 'medicos', + 'octavo' => 'octavos', + 'photo' => 'photos', + 'pro' => 'pros', + 'quarto' => 'quartos', + 'canto' => 'cantos', + 'lingo' => 'lingos', + 'generalissimo' => 'generalissimos', + 'stylo' => 'stylos', + 'rhino' => 'rhinos', + 'casino' => 'casinos', + 'auto' => 'autos', + 'macro' => 'macros', + 'zero' => 'zeros', + 'todo' => 'todos', + 'studio' => 'studios', + 'avocado' => 'avocados', + 'zoo' => 'zoos', + 'banjo' => 'banjos', + 'cargo' => 'cargos', + 'flamingo' => 'flamingos', + 'fresco' => 'frescos', + 'halo' => 'halos', + 'mango' => 'mangos', + 'memento' => 'mementos', + 'motto' => 'mottos', + 'tornado' => 'tornados', + 'tuxedo' => 'tuxedos', + 'volcano' => 'volcanos', + # The correct form from italian is: o => i. (Eg. contralto => contralti) + # English dictionaries are reporting o => s as a valid rule + # + # We're sticking to the latter rule, in order to not introduce exceptions + # for words that end with "o". See the previous category. + 'solo' => 'solos', + 'soprano' => 'sopranos', + 'basso' => 'bassos', + 'alto' => 'altos', + 'contralto' => 'contraltos', + 'tempo' => 'tempos', + 'piano' => 'pianos', + 'virtuoso' => 'virtuosos', + # o => oes + 'buffalo' => 'buffaloes', + 'domino' => 'dominoes', + 'echo' => 'echoes', + 'embargo' => 'embargoes', + 'hero' => 'heroes', + 'mosquito' => 'mosquitoes', + 'potato' => 'potatoes', + 'tomato' => 'tomatoes', + 'torpedo' => 'torpedos', + 'veto' => 'vetos', + # a => ata + 'anathema' => 'anathemata', + 'enema' => 'enemata', + 'oedema' => 'oedemata', + 'bema' => 'bemata', + 'enigma' => 'enigmata', + 'sarcoma' => 'sarcomata', + 'carcinoma' => 'carcinomata', + 'gumma' => 'gummata', + 'schema' => 'schemata', + 'charisma' => 'charismata', + 'lemma' => 'lemmata', + 'soma' => 'somata', + 'diploma' => 'diplomata', + 'lymphoma' => 'lymphomata', + 'stigma' => 'stigmata', + 'dogma' => 'dogmata', + 'magma' => 'magmata', + 'stoma' => 'stomata', + 'drama' => 'dramata', + 'melisma' => 'melismata', + 'trauma' => 'traumata', + 'edema' => 'edemata', + 'miasma' => 'miasmata', + # # is => es + # "axis" => "axes", + # "analysis" => "analyses", + # "basis" => "bases", + # "crisis" => "crises", + # "diagnosis" => "diagnoses", + # "ellipsis" => "ellipses", + # "hypothesis" => "hypotheses", + # "oasis" => "oases", + # "paralysis" => "paralyses", + # "parenthesis" => "parentheses", + # "synthesis" => "syntheses", + # "synopsis" => "synopses", + # "thesis" => "theses", + # us => uses + 'apparatus' => 'apparatuses', + 'impetus' => 'impetuses', + 'prospectus' => 'prospectuses', + 'cantus' => 'cantuses', + 'nexus' => 'nexuses', + 'sinus' => 'sinuses', + 'coitus' => 'coituses', + 'plexus' => 'plexuses', + 'status' => 'statuses', + 'hiatus' => 'hiatuses', + 'bus' => 'buses', + 'octopus' => 'octopuses', + # + # none => i + # "afreet" => true, + # "afrit" => true, + # "efreet" => true, + # + # none => im + # "cherub" => true, + # "goy" => true, + # "seraph" => true, + # + # man => mans + 'human' => 'humans', + 'Alabaman' => 'Alabamans', + 'Bahaman' => 'Bahamans', + 'Burman' => 'Burmans', + 'German' => 'Germans', + 'Hiroshiman' => 'Hiroshimans', + 'Liman' => 'Limans', + 'Nakayaman' => 'Nakayamans', + 'Oklahoman' => 'Oklahomans', + 'Panaman' => 'Panamans', + 'Selman' => 'Selmans', + 'Sonaman' => 'Sonamans', + 'Tacoman' => 'Tacomans', + 'Yakiman' => 'Yakimans', + 'Yokohaman' => 'Yokohamans', + 'Yuman' => 'Yumans', + # ch => es + 'witch' => 'witches', + 'church' => 'churches', + # ch => chs + 'stomach' => 'stomachs', + 'epoch' => 'epochs', + # e => es, + 'mustache' => 'mustaches', + 'horse' => 'horses', + 'verse' => 'verses', + 'universe' => 'universes', + 'inverse' => 'inverses', + 'price' => 'prices', + 'advice' => 'advices', + 'device' => 'devices', + # x => es + 'box' => 'boxes', + 'fox' => 'foxes', + # vowel + y => s + 'boy' => 'boys', + 'way' => 'ways', + 'buy' => 'buys', + # consonant + y => ies + 'baby' => 'babies', + 'lorry' => 'lorries', + 'entity' => 'entities', + 'repository' => 'repositories', + 'fly' => 'flies', + # f => ves + 'leaf' => 'leaves', + 'hoof' => 'hooves', + 'self' => 'selves', + 'elf' => 'elves', + 'half' => 'halves', + 'scarf' => 'scarves', + 'dwarf' => 'dwarves', + # vocal + fe => ves + 'knife' => 'knives', + 'life' => 'lives', + 'wife' => 'wives', + # eau => eaux + 'beau' => 'beaux', + 'bureau' => 'bureaux', + 'tableau' => 'tableaux', + # ouse => ice + 'louse' => 'lice', + 'mouse' => 'mice', + # irregular + 'cactus' => 'cacti', + 'foot' => 'feet', + 'tooth' => 'teeth', + 'goose' => 'geese', + 'child' => 'children', + 'man' => 'men', + 'woman' => 'women', + 'person' => 'people', + 'ox' => 'oxen', + 'corpus' => 'corpora', + 'genus' => 'genera', + 'sex' => 'sexes', + 'quiz' => 'quizzes', + 'testis' => 'testes', + # uncountable + 'deer' => 'deer', + 'fish' => 'fish', + 'money' => 'money', + 'means' => 'means', + 'offspring' => 'offspring', + 'series' => 'series', + 'sheep' => 'sheep', + 'species' => 'species', + 'equipment' => 'equipment', + 'information' => 'information', + 'rice' => 'rice', + 'news' => 'news', + 'police' => 'police', + # fallback (add s) + 'giraffe' => 'giraffes', + 'test' => 'tests', + 'feature' => 'features', + 'fixture' => 'fixtures', + 'controller' => 'controllers', + 'action' => 'actions', + 'router' => 'routers', + 'route' => 'routes', + 'endpoint' => 'endpoints', + 'string' => 'strings', + 'view' => 'views', + 'template' => 'templates', + 'layout' => 'layouts', + 'application' => 'applications', + 'api' => 'apis', + 'model' => 'models', + 'mapper' => 'mappers', + 'mapping' => 'mappings', + 'table' => 'tables', + 'attribute' => 'attributes', + 'column' => 'columns', + 'migration' => 'migrations', + 'presenter' => 'presenters', + 'wizard' => 'wizards', + 'architecture' => 'architectures', + 'cat' => 'cats', + 'car' => 'cars', + 'hive' => 'hives', + # https://github.com/hanami/utils/issues/106 + 'album' => 'albums', + # https://github.com/hanami/utils/issues/173 + 'kitten' => 'kittens' +}.freeze + +TEST_SINGULARS = { + # a => ae + 'alumna' => 'alumnae', + 'alga' => 'algae', + 'vertebra' => 'vertebrae', + 'persona' => 'personae', + 'antenna' => 'antennae', + 'formula' => 'formulae', + 'nebula' => 'nebulae', + 'vita' => 'vitae', + # is => ides + 'iris' => 'irides', + 'clitoris' => 'clitorides', + # us => i + 'alumnus' => 'alumni', + 'alveolus' => 'alveoli', + 'bacillus' => 'bacilli', + 'bronchus' => 'bronchi', + 'locus' => 'loci', + 'nucleus' => 'nuclei', + 'stimulus' => 'stimuli', + 'meniscus' => 'menisci', + 'thesaurus' => 'thesauri', + # f => s + 'chief' => 'chiefs', + 'spoof' => 'spoofs', + # en => ina + 'stamen' => 'stamina', + 'foramen' => 'foramina', + 'lumen' => 'lumina', + # s => es + 'acropolis' => 'acropolises', + 'chaos' => 'chaoses', + 'lens' => 'lenses', + 'aegis' => 'aegises', + 'cosmos' => 'cosmoses', + 'mantis' => 'mantises', + 'alias' => 'aliases', + 'dais' => 'daises', + 'marquis' => 'marquises', + 'asbestos' => 'asbestoses', + 'digitalis' => 'digitalises', + 'metropolis' => 'metropolises', + 'atlas' => 'atlases', + 'epidermis' => 'epidermises', + 'pathos' => 'pathoses', + 'bathos' => 'bathoses', + 'ethos' => 'ethoses', + 'pelvis' => 'pelvises', + 'bias' => 'biases', + 'gas' => 'gases', + 'polis' => 'polises', + 'caddis' => 'caddises', + 'rhinoceros' => 'rhinoceroses', + 'cannabis' => 'cannabises', + 'glottis' => 'glottises', + 'sassafras' => 'sassafrases', + 'canvas' => 'canvases', + 'ibis' => 'ibises', + 'trellis' => 'trellises', + 'kiss' => 'kisses', + # https://github.com/hanami/utils/issues/106 + 'album' => 'albums' +}.merge(TEST_PLURALS) + +require 'hanami/utils/inflector' +Hanami::Utils::Inflector.inflections do + exception 'analysis', 'analyses' + exception 'alga', 'algae' + uncountable 'music', 'butter' +end + +class WrappingHash + def initialize(hash) + @hash = hash.to_h + end + + def to_hash + @hash + end + alias to_h to_hash +end diff --git a/spec/support/isolation_spec_helper.rb b/spec/support/isolation_spec_helper.rb new file mode 100644 index 0000000..4147d4a --- /dev/null +++ b/spec/support/isolation_spec_helper.rb @@ -0,0 +1,17 @@ +require 'rubygems' +require 'bundler' +Bundler.setup(:default, :development) + +$LOAD_PATH.unshift 'lib' +require 'hanami/utils' +require_relative './rspec' + +module RSpec + module Support + module Runner + def self.run + Core::Runner.autorun + end + end + end +end diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb new file mode 100644 index 0000000..dc993bd --- /dev/null +++ b/spec/support/rspec.rb @@ -0,0 +1,25 @@ +require 'rspec' + +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + + config.shared_context_metadata_behavior = :apply_to_host_groups + + config.filter_run_when_matching :focus + config.disable_monkey_patching! + + config.warnings = true + + config.default_formatter = 'doc' if config.files_to_run.one? + + config.profile_examples = 10 + + config.order = :random + Kernel.srand config.seed +end diff --git a/spec/support/stdout.rb b/spec/support/stdout.rb new file mode 100644 index 0000000..9b6cd61 --- /dev/null +++ b/spec/support/stdout.rb @@ -0,0 +1,19 @@ +module RSpec + module Support + module Stdout + def with_captured_stdout + original = $stdout + captured = StringIO.new + $stdout = captured + yield + $stdout.string + ensure + $stdout = original + end + end + end +end + +RSpec.configure do |config| + config.include RSpec::Support::Stdout +end diff --git a/test/class_attribute_test.rb b/test/class_attribute_test.rb index d44cf9a..231c595 100644 --- a/test/class_attribute_test.rb +++ b/test/class_attribute_test.rb @@ -12,7 +12,7 @@ describe Hanami::Utils::ClassAttribute do class SubclassAttributeTest < ClassAttributeTest class_attribute :subattribute - self.functions = [:x, :y] + self.functions = %i(x y) self.subattribute = 42 end @@ -44,15 +44,15 @@ describe Hanami::Utils::ClassAttribute do end after do - [:ClassAttributeTest, - :SubclassAttributeTest, - :SubSubclassAttributeTest, - :Vehicle, - :Car, - :Airplane, - :SmallAirplane].each do |const| - Object.send :remove_const, const - end + %i(ClassAttributeTest + SubclassAttributeTest + SubSubclassAttributeTest + Vehicle + Car + Airplane + SmallAirplane).each do |const| + Object.send :remove_const, const + end end it 'sets the given value' do @@ -75,7 +75,7 @@ describe Hanami::Utils::ClassAttribute do it 'if the superclass value changes it does not affects subclasses' do ClassAttributeTest.functions = [:y] - SubclassAttributeTest.functions.must_equal([:x, :y]) + SubclassAttributeTest.functions.must_equal(%i(x y)) end it 'if the subclass value changes it does not affects superclass' do diff --git a/test/interactor_test.rb b/test/interactor_test.rb index 6abe71b..ab8f3e5 100644 --- a/test/interactor_test.rb +++ b/test/interactor_test.rb @@ -242,7 +242,7 @@ describe Hanami::Interactor do it "doesn't interrupt the flow" do result = ErrorInteractor.new.call - result.operations.must_equal [:prepare!, :persist!, :log!] + result.operations.must_equal %i(prepare! persist! log!) end # See https://github.com/hanami/utils/issues/69 diff --git a/test/isolation/reload_test.rb b/test/isolation/reload_test.rb index 11c2622..9d66abc 100644 --- a/test/isolation/reload_test.rb +++ b/test/isolation/reload_test.rb @@ -17,12 +17,12 @@ describe 'Hanami::Utils.reload!' do it 'reloads the files set of files' do File.open(root.join('user.rb'), 'w+') do |f| f.write <<-EOF -class User - def greet - "Hi" - end -end -EOF + class User + def greet + "Hi" + end + end + EOF end Hanami::Utils.reload!(root) @@ -30,12 +30,12 @@ EOF File.open(root.join('user.rb'), 'w+') do |f| f.write <<-EOF -class User - def greet - "Ciao" - end -end -EOF + class User + def greet + "Ciao" + end + end + EOF end Hanami::Utils.reload!(root)