diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 953cc7013..efeab9dec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,14 +24,17 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - bundler-cache: true + bundler-cache: false - name: Install sqlite run: | sudo apt-get install libsqlite3-dev + - name: Install dependencies + run: bundle install + - name: Run Tests run: INTEGRATION_TESTS=1 bundle exec rspec - - name: Rubocop - run: bundle exec rubocop + # - name: Rubocop + # run: bundle exec rubocop diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 246a23ea6..2df92fdbe 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -27,7 +27,7 @@ Gemspec/RequiredRubyVersion: # SupportedHashRocketStyles: key, separator, table # SupportedColonStyles: key, separator, table # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit -Layout/AlignHash: +Layout/HashAlignment: Exclude: - 'lib/generators/annotate/templates/auto_annotate_models.rake' - 'spec/lib/annotate/annotate_models_spec.rb' @@ -68,7 +68,7 @@ Layout/ExtraSpacing: # Cop supports --auto-correct. # Configuration parameters: IndentationWidth. # SupportedStyles: special_inside_parentheses, consistent, align_brackets -Layout/IndentFirstArrayElement: +Layout/FirstArrayElementIndentation: EnforcedStyle: consistent # Offense count: 5 @@ -158,7 +158,7 @@ Lint/AssignmentInCondition: - 'lib/annotate/annotate_models.rb' # Offense count: 1 -Lint/HandleExceptions: +Lint/SuppressedException: Exclude: - 'bin/annotate' @@ -237,7 +237,7 @@ Naming/MemoizedInstanceVariableName: # Offense count: 1 # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. # AllowedNames: io, id, to, by, on, in, at, ip, db -Naming/UncommunicativeMethodParamName: +Naming/MethodParameterName: Exclude: - 'Rakefile' @@ -354,7 +354,7 @@ Style/InverseMethods: - 'Rakefile' # Offense count: 1 -Style/MethodMissingSuper: +Lint/MissingSuper: Exclude: - 'lib/annotate/active_record_patch.rb' @@ -522,7 +522,7 @@ Style/TrailingCommaInArrayLiteral: # Offense count: 2 # Cop supports --auto-correct. -Style/UnneededPercentQ: +Style/RedundantPercentQ: Exclude: - 'annotate.gemspec' diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 000000000..9b6768d19 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +ruby 2.6.7 diff --git a/Gemfile b/Gemfile index 184fe0ba4..4acd9cb84 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -ruby '>= 2.2.0' +ruby '>= 2.4.0' gem 'activerecord', '>= 4.2.5', '< 6', require: false gem 'rake', require: false @@ -20,7 +20,10 @@ group :development, :test do gem 'rspec', require: false gem 'rubocop', '~> 0.68.1', require: false unless RUBY_VERSION =~ /^1.8/ - gem 'simplecov', require: false + # gem 'rubocop', '~> 1.12', require: false + # gem 'rubocop-rake', require: false + # gem 'rubocop-rspec', require: false + # gem 'simplecov', require: false gem 'terminal-notifier-guard', require: false gem 'codeclimate-test-reporter' diff --git a/Guardfile b/Guardfile index 602c7b57c..f93bc135e 100644 --- a/Guardfile +++ b/Guardfile @@ -1,4 +1,4 @@ -# Note: The cmd option is now required due to the increasing number of ways +# NOTE: The cmd option is now required due to the increasing number of ways # rspec may be run, below are examples of the most common uses. # * bundler: 'bundle exec rspec' # * bundler binstubs: 'bin/rspec' diff --git a/Rakefile b/Rakefile index bfbd51b7d..43944f2dc 100644 --- a/Rakefile +++ b/Rakefile @@ -3,7 +3,7 @@ def exit_exception(e) exit e.status_code end -# Note : this causes annoying psych warnings under Ruby 1.9.2-p180; to fix, upgrade to 1.9.3 +# NOTE: this causes annoying psych warnings under Ruby 1.9.2-p180; to fix, upgrade to 1.9.3 begin require 'bundler' Bundler.setup(:default, :development) @@ -162,7 +162,7 @@ namespace :integration do fixtures[Digest::MD5.hexdigest(File.read(fname))] = File.expand_path(fname) end - candidates.keys.each do |digest| + candidates.each_key do |digest| next unless fixtures.key?(digest) candidates[digest].each do |fname| # Double-check contents in case of hash collision... diff --git a/lib/annotate/annotate_models.rb b/lib/annotate/annotate_models.rb index 1d84b7945..abcc41a47 100644 --- a/lib/annotate/annotate_models.rb +++ b/lib/annotate/annotate_models.rb @@ -53,7 +53,7 @@ def model_dir @model_dir.is_a?(Array) ? @model_dir : [@model_dir || 'app/models'] end - attr_writer :model_dir + attr_writer :model_dir, :root_dir, :skip_subdirectory_model_load def root_dir if @root_dir.blank? @@ -65,8 +65,6 @@ def root_dir end end - attr_writer :root_dir - def skip_subdirectory_model_load # This option is set in options[:skip_subdirectory_model_load] # and stops the get_loaded_model method from loading a model from a subdir @@ -78,8 +76,6 @@ def skip_subdirectory_model_load end end - attr_writer :skip_subdirectory_model_load - def get_patterns(options, pattern_types = []) current_patterns = [] root_dir.each do |root_directory| @@ -89,7 +85,7 @@ def get_patterns(options, pattern_types = []) current_patterns += if pattern_type.to_sym == :additional_file_patterns patterns else - patterns.map { |p| p.sub(/^[\/]*/, '') } + patterns.map { |p| p.sub(/^\/*/, '') } end end end @@ -156,15 +152,15 @@ def get_schema_info(klass, header, options = {}) end if options[:format_rdoc] - info << sprintf("# %-#{max_size}.#{max_size}s%s", "*#{col_name}*::", attrs.unshift(col_type).join(", ")).rstrip + "\n" + info << "#{sprintf("# %-#{max_size}.#{max_size}s%s", "*#{col_name}*::", attrs.unshift(col_type).join(", ")).rstrip}\n" elsif options[:format_yard] - info << sprintf("# @!attribute #{col_name}") + "\n" + info << "#{sprintf("# @!attribute #{col_name}")}\n" ruby_class = col.respond_to?(:array) && col.array ? "Array<#{map_col_type_to_ruby_classes(col_type)}>": map_col_type_to_ruby_classes(col_type) - info << sprintf("# @return [#{ruby_class}]") + "\n" + info << "#{sprintf("# @return [#{ruby_class}]")}\n" elsif options[:format_markdown] name_remainder = max_size - col_name.length - non_ascii_length(col_name) type_remainder = (md_type_allowance - 2) - col_type.length - info << (sprintf("# **`%s`**%#{name_remainder}s | `%s`%#{type_remainder}s | `%s`", col_name, " ", col_type, " ", attrs.join(", ").rstrip)).gsub('``', ' ').rstrip + "\n" + info << "#{(sprintf("# **`%s`**%#{name_remainder}s | `%s`%#{type_remainder}s | `%s`", col_name, " ", col_type, " ", attrs.join(", ").rstrip)).gsub('``', ' ').rstrip}\n" else info << format_default(col_name, max_size, col_type, bare_type_allowance, attrs) end @@ -345,7 +341,7 @@ def get_foreign_key_info(klass, options = {}) fk_info << if options[:format_markdown] sprintf("# * `%s`%s:\n# * **`%s`**\n", format_name.call(fk), constraints_info.blank? ? '' : " (_#{constraints_info}_)", ref_info) else - sprintf("# %-#{max_size}.#{max_size}s %s %s", format_name.call(fk), "(#{ref_info})", constraints_info).rstrip + "\n" + "#{sprintf("# %-#{max_size}.#{max_size}s %s %s", format_name.call(fk), "(#{ref_info})", constraints_info).rstrip}\n" end end @@ -371,11 +367,11 @@ def annotate_one_file(file_name, info_block, position, options = {}) return false if old_content =~ /#{SKIP_ANNOTATION_PREFIX}.*\n/ # Ignore the Schema version line because it changes with each migration - header_pattern = /(^# Table name:.*?\n(#.*[\r]?\n)*[\r]?)/ + header_pattern = /(^# Table name:.*?\n(#.*\r?\n)*\r?)/ old_header = old_content.match(header_pattern).to_s new_header = info_block.match(header_pattern).to_s - column_pattern = /^#[\t ]+[\w\*\.`]+[\t ]+.+$/ + column_pattern = /^#[\t ]+[\w*.`]+[\t ]+.+$/ old_columns = old_header && old_header.scan(column_pattern).sort new_columns = new_header && new_header.scan(column_pattern).sort @@ -398,11 +394,11 @@ def annotate_one_file(file_name, info_block, position, options = {}) old_content.sub!(annotate_pattern(options), '') new_content = if %w(after bottom).include?(options[position].to_s) - magic_comments_block + (old_content.rstrip + "\n\n" + wrapped_info_block) + magic_comments_block + ("#{old_content.rstrip}\n\n#{wrapped_info_block}") elsif magic_comments_block.empty? magic_comments_block + wrapped_info_block + old_content.lstrip else - magic_comments_block + "\n" + wrapped_info_block + old_content.lstrip + "#{magic_comments_block}\n#{wrapped_info_block}#{old_content.lstrip}" end else # replace the old annotation with the new one @@ -512,7 +508,7 @@ def annotate(klass, file, header, options = {}) end rescue StandardError => e $stderr.puts "Unable to annotate #{file}: #{e.message}" - $stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace] + $stderr.puts "\t#{e.backtrace.join("\n\t")}" if options[:trace] end annotated @@ -589,7 +585,7 @@ def get_model_class(file) if File.file?(file_path) && Kernel.require(file_path) retry elsif model_path =~ /\// - model_path = model_path.split('/')[1..-1].join('/').to_s + model_path = model_path.split('/')[1..].join('/').to_s retry else raise @@ -625,7 +621,7 @@ def get_loaded_model_by_path(model_path) # Revert to the old way but it is not really robust ObjectSpace.each_object(::Class) .select do |c| - Class === c && # note: we use === to avoid a bug in activesupport 2.3.14 OptionMerger vs. is_a? + Class === c && # NOTE: we use === to avoid a bug in activesupport 2.3.14 OptionMerger vs. is_a? c.ancestors.respond_to?(:include?) && # to fix FactoryGirl bug, see https://github.com/ctran/annotate_models/pull/82 c.ancestors.include?(ActiveRecord::Base) end.detect { |c| ActiveSupport::Inflector.underscore(c.to_s) == model_path } @@ -685,11 +681,11 @@ def annotate_model_file(annotated, file, header, options) rescue BadModelFileError => e unless options[:ignore_unknown_models] $stderr.puts "Unable to annotate #{file}: #{e.message}" - $stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace] + $stderr.puts "\t#{e.backtrace.join("\n\t")}" if options[:trace] end rescue StandardError => e $stderr.puts "Unable to annotate #{file}: #{e.message}" - $stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace] + $stderr.puts "\t#{e.backtrace.join("\n\t")}" if options[:trace] end end @@ -720,7 +716,7 @@ def remove_annotations(options = {}) deannotated << klass if deannotated_klass rescue StandardError => e $stderr.puts "Unable to deannotate #{File.join(file)}: #{e.message}" - $stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace] + $stderr.puts "\t#{e.backtrace.join("\n\t")}" if options[:trace] end end puts "Removed annotations from: #{deannotated.join(', ')}" @@ -780,7 +776,7 @@ def max_schema_info_width(klass, options) end def format_default(col_name, max_size, col_type, bare_type_allowance, attrs) - sprintf("# %s:%s %s", mb_chars_ljust(col_name, max_size), mb_chars_ljust(col_type, bare_type_allowance), attrs.join(", ")).rstrip + "\n" + "#{sprintf("# %s:%s %s", mb_chars_ljust(col_name, max_size), mb_chars_ljust(col_type, bare_type_allowance), attrs.join(", ")).rstrip}\n" end def width(string) diff --git a/lib/annotate/annotate_routes.rb b/lib/annotate/annotate_routes.rb index 75cc421ed..38499d5e9 100644 --- a/lib/annotate/annotate_routes.rb +++ b/lib/annotate/annotate_routes.rb @@ -67,9 +67,10 @@ def routes_file end def strip_on_removal(content, header_position) - if header_position == :before + case header_position + when :before content.shift while content.first == '' - elsif header_position == :after + when :after content.pop while content.last == '' end diff --git a/lib/annotate/annotate_routes/header_generator.rb b/lib/annotate/annotate_routes/header_generator.rb index b1c93acf7..4cd7c60b1 100644 --- a/lib/annotate/annotate_routes/header_generator.rb +++ b/lib/annotate/annotate_routes/header_generator.rb @@ -21,7 +21,7 @@ def routes_map(options) # In old versions of Rake, the first line of output was the cwd. Not so # much in newer ones. We ditch that line if it exists, and if not, we # keep the line around. - result.shift if result.first =~ %r{^\(in \/} + result.shift if result.first =~ %r{^\(in /} ignore_routes = options[:ignore_routes] regexp_for_ignoring_routes = ignore_routes ? /#{ignore_routes}/ : nil @@ -57,7 +57,7 @@ def generate out << comment return out if contents_without_magic_comments.size.zero? - maxs = [HEADER_ROW.map(&:size)] + contents_without_magic_comments[1..-1].map { |line| line.split.map(&:size) } + maxs = [HEADER_ROW.map(&:size)] + contents_without_magic_comments[1..].map { |line| line.split.map(&:size) } if markdown? max = maxs.map(&:max).compact.max @@ -68,7 +68,7 @@ def generate out << comment(content(contents_without_magic_comments[0], maxs)) end - out += contents_without_magic_comments[1..-1].map { |line| comment(content(markdown? ? line.split(' ') : line, maxs)) } + out += contents_without_magic_comments[1..].map { |line| comment(content(markdown? ? line.split(' ') : line, maxs)) } out << comment(options[:wrapper_close]) if options[:wrapper_close] out diff --git a/spec/lib/annotate/annotate_models_spec.rb b/spec/lib/annotate/annotate_models_spec.rb index 370298f3c..d286cbb72 100644 --- a/spec/lib/annotate/annotate_models_spec.rb +++ b/spec/lib/annotate/annotate_models_spec.rb @@ -1,2503 +1,28 @@ # encoding: utf-8 require_relative '../../spec_helper' +require_relative 'models/model_spec_helper' require 'annotate/annotate_models' require 'annotate/active_record_patch' require 'active_support/core_ext/string' require 'files' require 'tmpdir' -describe AnnotateModels do - MAGIC_COMMENTS = [ - '# encoding: UTF-8', - '# coding: UTF-8', - '# -*- coding: UTF-8 -*-', - '#encoding: utf-8', - '# encoding: utf-8', - '# -*- encoding : utf-8 -*-', - "# encoding: utf-8\n# frozen_string_literal: true", - "# frozen_string_literal: true\n# encoding: utf-8", - '# frozen_string_literal: true', - '#frozen_string_literal: false', - '# -*- frozen_string_literal : true -*-' - ].freeze - - def mock_index(name, params = {}) - double('IndexKeyDefinition', - name: name, - columns: params[:columns] || [], - unique: params[:unique] || false, - orders: params[:orders] || {}, - where: params[:where], - using: params[:using]) - end - - def mock_foreign_key(name, from_column, to_table, to_column = 'id', constraints = {}) - double('ForeignKeyDefinition', - name: name, - column: from_column, - to_table: to_table, - primary_key: to_column, - on_delete: constraints[:on_delete], - on_update: constraints[:on_update]) - end - - def mock_connection(indexes = [], foreign_keys = []) - double('Conn', - indexes: indexes, - foreign_keys: foreign_keys, - supports_foreign_keys?: true) - end - - def mock_class(table_name, primary_key, columns, indexes = [], foreign_keys = []) - options = { - connection: mock_connection(indexes, foreign_keys), - table_exists?: true, - table_name: table_name, - primary_key: primary_key, - column_names: columns.map { |col| col.name.to_s }, - columns: columns, - column_defaults: Hash[columns.map { |col| [col.name, col.default] }], - table_name_prefix: '' - } - - double('An ActiveRecord class', options) - end - - def mock_column(name, type, options = {}) - default_options = { - limit: nil, - null: false, - default: nil, - sql_type: type - } - - stubs = default_options.dup - stubs.merge!(options) - stubs[:name] = name - stubs[:type] = type - - double('Column', stubs) - end - - describe '.quote' do - subject do - AnnotateModels.quote(value) - end - - context 'when the argument is nil' do - let(:value) { nil } - it 'returns string "NULL"' do - is_expected.to eq('NULL') - end - end - - context 'when the argument is true' do - let(:value) { true } - it 'returns string "TRUE"' do - is_expected.to eq('TRUE') - end - end - - context 'when the argument is false' do - let(:value) { false } - it 'returns string "FALSE"' do - is_expected.to eq('FALSE') - end - end - - context 'when the argument is an integer' do - let(:value) { 25 } - it 'returns the integer as a string' do - is_expected.to eq('25') - end - end - - context 'when the argument is a float number' do - context 'when the argument is like 25.6' do - let(:value) { 25.6 } - it 'returns the float number as a string' do - is_expected.to eq('25.6') - end - end - - context 'when the argument is like 1e-20' do - let(:value) { 1e-20 } - it 'returns the float number as a string' do - is_expected.to eq('1.0e-20') - end - end - end - - context 'when the argument is a BigDecimal number' do - let(:value) { BigDecimal('1.2') } - it 'returns the float number as a string' do - is_expected.to eq('1.2') - end - end - - context 'when the argument is an array' do - let(:value) { [BigDecimal('1.2')] } - it 'returns an array of which elements are converted to string' do - is_expected.to eq(['1.2']) - end - end - end - - describe '.parse_options' do - let(:options) do - { - root_dir: '/root', - model_dir: 'app/models,app/one, app/two ,,app/three', - skip_subdirectory_model_load: false - } - end - - before :each do - AnnotateModels.send(:parse_options, options) - end - - describe '@root_dir' do - subject do - AnnotateModels.instance_variable_get(:@root_dir) - end - - it 'sets @root_dir' do - is_expected.to eq('/root') - end - end - - describe '@model_dir' do - subject do - AnnotateModels.instance_variable_get(:@model_dir) - end - - it 'separates option "model_dir" with commas and sets @model_dir as an array of string' do - is_expected.to eq(['app/models', 'app/one', 'app/two', 'app/three']) - end - end - - describe '@skip_subdirectory_model_load' do - subject do - AnnotateModels.instance_variable_get(:@skip_subdirectory_model_load) - end - - context 'option is set to true' do - let(:options) do - { - root_dir: '/root', - model_dir: 'app/models,app/one, app/two ,,app/three', - skip_subdirectory_model_load: true - } - end - - it 'sets skip_subdirectory_model_load to true' do - is_expected.to eq(true) - end - end - - context 'option is set to false' do - let(:options) do - { - root_dir: '/root', - model_dir: 'app/models,app/one, app/two ,,app/three', - skip_subdirectory_model_load: false - } - end - - it 'sets skip_subdirectory_model_load to false' do - is_expected.to eq(false) - end - end - end - end - - describe '.get_schema_info' do - subject do - AnnotateModels.get_schema_info(klass, header, **options) - end - - let :klass do - mock_class(:users, primary_key, columns, indexes, foreign_keys) - end - - let :indexes do - [] - end - - let :foreign_keys do - [] - end - - context 'when option is not present' do - let :options do - {} - end - - context 'when header is "Schema Info"' do - let :header do - 'Schema Info' - end - - context 'when the primary key is not specified' do - let :primary_key do - nil - end - - context 'when the columns are normal' do - let :columns do - [ - mock_column(:id, :integer), - mock_column(:name, :string, limit: 50) - ] - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # id :integer not null - # name :string(50) not null - # - EOS - end - - it 'returns schema info' do - is_expected.to eq(expected_result) - end - end - - context 'when an enum column exists' do - let :columns do - [ - mock_column(:id, :integer), - mock_column(:name, :enum, limit: [:enum1, :enum2]) - ] - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # id :integer not null - # name :enum not null, (enum1, enum2) - # - EOS - end - - it 'returns schema info' do - is_expected.to eq(expected_result) - end - end - - context 'when unsigned columns exist' do - let :columns do - [ - mock_column(:id, :integer), - mock_column(:integer, :integer, unsigned?: true), - mock_column(:bigint, :integer, unsigned?: true, bigint?: true), - mock_column(:bigint, :bigint, unsigned?: true), - mock_column(:float, :float, unsigned?: true), - mock_column(:decimal, :decimal, unsigned?: true, precision: 10, scale: 2), - ] - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # id :integer not null - # integer :integer unsigned, not null - # bigint :bigint unsigned, not null - # bigint :bigint unsigned, not null - # float :float unsigned, not null - # decimal :decimal(10, 2) unsigned, not null - # - EOS - end - - it 'returns schema info' do - is_expected.to eq(expected_result) - end - end - end - - context 'when the primary key is specified' do - context 'when the primary_key is :id' do - let :primary_key do - :id - end - - context 'when columns are normal' do - let :columns do - [ - mock_column(:id, :integer, limit: 8), - mock_column(:name, :string, limit: 50), - mock_column(:notes, :text, limit: 55) - ] - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # id :integer not null, primary key - # name :string(50) not null - # notes :text(55) not null - # - EOS - end - - it 'returns schema info' do - is_expected.to eq(expected_result) - end - end - - context 'when columns have default values' do - let :columns do - [ - mock_column(:id, :integer), - mock_column(:size, :integer, default: 20), - mock_column(:flag, :boolean, default: false) - ] - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # id :integer not null, primary key - # size :integer default(20), not null - # flag :boolean default(FALSE), not null - # - EOS - end - - it 'returns schema info with default values' do - is_expected.to eq(expected_result) - end - end - - context 'with Globalize gem' do - let :translation_klass do - double('Post::Translation', - to_s: 'Post::Translation', - columns: [ - mock_column(:id, :integer, limit: 8), - mock_column(:post_id, :integer, limit: 8), - mock_column(:locale, :string, limit: 50), - mock_column(:title, :string, limit: 50), - ]) - end - - let :klass do - mock_class(:posts, primary_key, columns, indexes, foreign_keys).tap do |mock_klass| - allow(mock_klass).to receive(:translation_class).and_return(translation_klass) - end - end - - let :columns do - [ - mock_column(:id, :integer, limit: 8), - mock_column(:author_name, :string, limit: 50), - ] - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: posts - # - # id :integer not null, primary key - # author_name :string(50) not null - # title :string(50) not null - # - EOS - end - - it 'returns schema info' do - is_expected.to eq expected_result - end - end - end - - context 'when the primary key is an array (using composite_primary_keys)' do - let :primary_key do - [:a_id, :b_id] - end - - let :columns do - [ - mock_column(:a_id, :integer), - mock_column(:b_id, :integer), - mock_column(:name, :string, limit: 50) - ] - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # a_id :integer not null, primary key - # b_id :integer not null, primary key - # name :string(50) not null - # - EOS - end - - it 'returns schema info' do - is_expected.to eq(expected_result) - end - end - end - end - end - - context 'when option is present' do - context 'when header is "Schema Info"' do - let :header do - 'Schema Info' - end - - context 'when the primary key is specified' do - context 'when the primary_key is :id' do - let :primary_key do - :id - end - - context 'when indexes exist' do - context 'when option "show_indexes" is true' do - let :options do - { show_indexes: true } - end - - context 'when indexes are normal' do - let :columns do - [ - mock_column(:id, :integer), - mock_column(:foreign_thing_id, :integer) - ] - end - - let :indexes do - [ - mock_index('index_rails_02e851e3b7', columns: ['id']), - mock_index('index_rails_02e851e3b8', columns: ['foreign_thing_id']) - ] - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # id :integer not null, primary key - # foreign_thing_id :integer not null - # - # Indexes - # - # index_rails_02e851e3b7 (id) - # index_rails_02e851e3b8 (foreign_thing_id) - # - EOS - end - - it 'returns schema info with index information' do - is_expected.to eq expected_result - end - end - - context 'when one of indexes includes orderd index key' do - let :columns do - [ - mock_column("id", :integer), - mock_column("firstname", :string), - mock_column("surname", :string), - mock_column("value", :string) - ] - end - - let :indexes do - [ - mock_index('index_rails_02e851e3b7', columns: ['id']), - mock_index('index_rails_02e851e3b8', - columns: %w(firstname surname value), - orders: { 'surname' => :asc, 'value' => :desc }) - ] - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # id :integer not null, primary key - # firstname :string not null - # surname :string not null - # value :string not null - # - # Indexes - # - # index_rails_02e851e3b7 (id) - # index_rails_02e851e3b8 (firstname,surname ASC,value DESC) - # - EOS - end - - it 'returns schema info with index information' do - is_expected.to eq expected_result - end - end - - context 'when one of indexes includes "where" clause' do - let :columns do - [ - mock_column("id", :integer), - mock_column("firstname", :string), - mock_column("surname", :string), - mock_column("value", :string) - ] - end - - let :indexes do - [ - mock_index('index_rails_02e851e3b7', columns: ['id']), - mock_index('index_rails_02e851e3b8', - columns: %w(firstname surname), - where: 'value IS NOT NULL') - ] - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # id :integer not null, primary key - # firstname :string not null - # surname :string not null - # value :string not null - # - # Indexes - # - # index_rails_02e851e3b7 (id) - # index_rails_02e851e3b8 (firstname,surname) WHERE value IS NOT NULL - # - EOS - end - - it 'returns schema info with index information' do - is_expected.to eq expected_result - end - end - - context 'when one of indexes includes "using" clause other than "btree"' do - let :columns do - [ - mock_column("id", :integer), - mock_column("firstname", :string), - mock_column("surname", :string), - mock_column("value", :string) - ] - end - - let :indexes do - [ - mock_index('index_rails_02e851e3b7', columns: ['id']), - mock_index('index_rails_02e851e3b8', - columns: %w(firstname surname), - using: 'hash') - ] - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # id :integer not null, primary key - # firstname :string not null - # surname :string not null - # value :string not null - # - # Indexes - # - # index_rails_02e851e3b7 (id) - # index_rails_02e851e3b8 (firstname,surname) USING hash - # - EOS - end - - it 'returns schema info with index information' do - is_expected.to eq expected_result - end - end - - context 'when index is not defined' do - let :columns do - [ - mock_column(:id, :integer), - mock_column(:foreign_thing_id, :integer) - ] - end - - let :indexes do - [] - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # id :integer not null, primary key - # foreign_thing_id :integer not null - # - EOS - end - - it 'returns schema info without index information' do - is_expected.to eq expected_result - end - end - end - - context 'when option "simple_indexes" is true' do - let :options do - { simple_indexes: true } - end - - context 'when one of indexes includes "orders" clause' do - let :columns do - [ - mock_column(:id, :integer), - mock_column(:foreign_thing_id, :integer) - ] - end - - let :indexes do - [ - mock_index('index_rails_02e851e3b7', columns: ['id']), - mock_index('index_rails_02e851e3b8', - columns: ['foreign_thing_id'], - orders: { 'foreign_thing_id' => :desc }) - ] - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # id :integer not null, primary key - # foreign_thing_id :integer not null - # - EOS - end - - it 'returns schema info with index information' do - is_expected.to eq expected_result - end - end - - context 'when one of indexes is in string form' do - let :columns do - [ - mock_column("id", :integer), - mock_column("name", :string) - ] - end - - let :indexes do - [ - mock_index('index_rails_02e851e3b7', columns: ['id']), - mock_index('index_rails_02e851e3b8', columns: 'LOWER(name)') - ] - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # id :integer not null, primary key, indexed - # name :string not null - # - EOS - end - - it 'returns schema info with index information' do - is_expected.to eq expected_result - end - end - end - end - - context 'when foreign keys exist' do - let :columns do - [ - mock_column(:id, :integer), - mock_column(:foreign_thing_id, :integer) - ] - end - - let :foreign_keys do - [ - mock_foreign_key('fk_rails_cf2568e89e', 'foreign_thing_id', 'foreign_things'), - mock_foreign_key('custom_fk_name', 'other_thing_id', 'other_things'), - mock_foreign_key('fk_rails_a70234b26c', 'third_thing_id', 'third_things') - ] - end - - context 'when option "show_foreign_keys" is specified' do - let :options do - { show_foreign_keys: true } - end - - context 'when foreign_keys does not have option' do - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # id :integer not null, primary key - # foreign_thing_id :integer not null - # - # Foreign Keys - # - # custom_fk_name (other_thing_id => other_things.id) - # fk_rails_... (foreign_thing_id => foreign_things.id) - # fk_rails_... (third_thing_id => third_things.id) - # - EOS - end - - it 'returns schema info with foreign keys' do - is_expected.to eq(expected_result) - end - end - - context 'when foreign_keys have option "on_delete" and "on_update"' do - let :foreign_keys do - [ - mock_foreign_key('fk_rails_02e851e3b7', - 'foreign_thing_id', - 'foreign_things', - 'id', - on_delete: 'on_delete_value', - on_update: 'on_update_value') - ] - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # id :integer not null, primary key - # foreign_thing_id :integer not null - # - # Foreign Keys - # - # fk_rails_... (foreign_thing_id => foreign_things.id) ON DELETE => on_delete_value ON UPDATE => on_update_value - # - EOS - end - - it 'returns schema info with foreign keys' do - is_expected.to eq(expected_result) - end - end - end - - context 'when option "show_foreign_keys" and "show_complete_foreign_keys" are specified' do - let :options do - { show_foreign_keys: true, show_complete_foreign_keys: true } - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # id :integer not null, primary key - # foreign_thing_id :integer not null - # - # Foreign Keys - # - # custom_fk_name (other_thing_id => other_things.id) - # fk_rails_a70234b26c (third_thing_id => third_things.id) - # fk_rails_cf2568e89e (foreign_thing_id => foreign_things.id) - # - EOS - end - - it 'returns schema info with foreign keys' do - is_expected.to eq(expected_result) - end - end - end - - context 'when "hide_limit_column_types" is specified in options' do - let :columns do - [ - mock_column(:id, :integer, limit: 8), - mock_column(:active, :boolean, limit: 1), - mock_column(:name, :string, limit: 50), - mock_column(:notes, :text, limit: 55) - ] - end - - context 'when "hide_limit_column_types" is blank string' do - let :options do - { hide_limit_column_types: '' } - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # id :integer not null, primary key - # active :boolean not null - # name :string(50) not null - # notes :text(55) not null - # - EOS - end - - it 'works with option "hide_limit_column_types"' do - is_expected.to eq expected_result - end - end - - context 'when "hide_limit_column_types" is "integer,boolean"' do - let :options do - { hide_limit_column_types: 'integer,boolean' } - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # id :integer not null, primary key - # active :boolean not null - # name :string(50) not null - # notes :text(55) not null - # - EOS - end - - it 'works with option "hide_limit_column_types"' do - is_expected.to eq expected_result - end - end - - context 'when "hide_limit_column_types" is "integer,boolean,string,text"' do - let :options do - { hide_limit_column_types: 'integer,boolean,string,text' } - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # id :integer not null, primary key - # active :boolean not null - # name :string not null - # notes :text not null - # - EOS - end - - it 'works with option "hide_limit_column_types"' do - is_expected.to eq expected_result - end - end - end - - context 'when "hide_default_column_types" is specified in options' do - let :columns do - [ - mock_column(:profile, :json, default: {}), - mock_column(:settings, :jsonb, default: {}), - mock_column(:parameters, :hstore, default: {}) - ] - end - - context 'when "hide_default_column_types" is blank string' do - let :options do - { hide_default_column_types: '' } - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # profile :json not null - # settings :jsonb not null - # parameters :hstore not null - # - EOS - end - - it 'works with option "hide_default_column_types"' do - is_expected.to eq expected_result - end - end - - context 'when "hide_default_column_types" is "skip"' do - let :options do - { hide_default_column_types: 'skip' } - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # profile :json default({}), not null - # settings :jsonb default({}), not null - # parameters :hstore default({}), not null - # - EOS - end - - it 'works with option "hide_default_column_types"' do - is_expected.to eq expected_result - end - end - - context 'when "hide_default_column_types" is "json"' do - let :options do - { hide_default_column_types: 'json' } - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # profile :json not null - # settings :jsonb default({}), not null - # parameters :hstore default({}), not null - # - EOS - end - - it 'works with option "hide_limit_column_types"' do - is_expected.to eq expected_result - end - end - end - - context 'when "classified_sort" is specified in options' do - let :columns do - [ - mock_column(:active, :boolean, limit: 1), - mock_column(:name, :string, limit: 50), - mock_column(:notes, :text, limit: 55) - ] - end - - context 'when "classified_sort" is "yes"' do - let :options do - { classified_sort: 'yes' } - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # active :boolean not null - # name :string(50) not null - # notes :text(55) not null - # - EOS - end - - it 'works with option "classified_sort"' do - is_expected.to eq expected_result - end - end - end - - context 'when "with_comment" is specified in options' do - context 'when "with_comment" is "yes"' do - let :options do - { with_comment: 'yes' } - end - - context 'when columns have comments' do - let :columns do - [ - mock_column(:id, :integer, limit: 8, comment: 'ID'), - mock_column(:active, :boolean, limit: 1, comment: 'Active'), - mock_column(:name, :string, limit: 50, comment: 'Name'), - mock_column(:notes, :text, limit: 55, comment: 'Notes'), - mock_column(:no_comment, :text, limit: 20, comment: nil) - ] - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # id(ID) :integer not null, primary key - # active(Active) :boolean not null - # name(Name) :string(50) not null - # notes(Notes) :text(55) not null - # no_comment :text(20) not null - # - EOS - end - - it 'works with option "with_comment"' do - is_expected.to eq expected_result - end - end - - context 'when columns have multibyte comments' do - let :columns do - [ - mock_column(:id, :integer, limit: 8, comment: 'ID'), - mock_column(:active, :boolean, limit: 1, comment: 'ACTIVE'), - mock_column(:name, :string, limit: 50, comment: 'NAME'), - mock_column(:notes, :text, limit: 55, comment: 'NOTES'), - mock_column(:cyrillic, :text, limit: 30, comment: 'Кириллица'), - mock_column(:japanese, :text, limit: 60, comment: '熊本大学 イタリア 宝島'), - mock_column(:arabic, :text, limit: 20, comment: 'لغة'), - mock_column(:no_comment, :text, limit: 20, comment: nil), - mock_column(:location, :geometry_collection, limit: nil, comment: nil) - ] - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # id(ID) :integer not null, primary key - # active(ACTIVE) :boolean not null - # name(NAME) :string(50) not null - # notes(NOTES) :text(55) not null - # cyrillic(Кириллица) :text(30) not null - # japanese(熊本大学 イタリア 宝島) :text(60) not null - # arabic(لغة) :text(20) not null - # no_comment :text(20) not null - # location :geometry_collect not null - # - EOS - end - - it 'works with option "with_comment"' do - is_expected.to eq expected_result - end - end - - context 'when columns have multiline comments' do - let :columns do - [ - mock_column(:id, :integer, limit: 8, comment: 'ID'), - mock_column(:notes, :text, limit: 55, comment: "Notes.\nMay include things like notes."), - mock_column(:no_comment, :text, limit: 20, comment: nil) - ] - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # id(ID) :integer not null, primary key - # notes(Notes.\\nMay include things like notes.):text(55) not null - # no_comment :text(20) not null - # - EOS - end - - it 'works with option "with_comment"' do - is_expected.to eq expected_result - end - end - - context 'when geometry columns are included' do - let :columns do - [ - mock_column(:id, :integer, limit: 8), - mock_column(:active, :boolean, default: false, null: false), - mock_column(:geometry, :geometry, - geometric_type: 'Geometry', srid: 4326, - limit: { srid: 4326, type: 'geometry' }), - mock_column(:location, :geography, - geometric_type: 'Point', srid: 0, - limit: { srid: 0, type: 'geometry' }) - ] - end - - let :expected_result do - <<~EOS - # Schema Info - # - # Table name: users - # - # id :integer not null, primary key - # active :boolean default(FALSE), not null - # geometry :geometry not null, geometry, 4326 - # location :geography not null, point, 0 - # - EOS - end - - it 'works with option "with_comment"' do - is_expected.to eq expected_result - end - end - end - end - end - end - end - - context 'when header is "== Schema Information"' do - let :header do - AnnotateModels::PREFIX - end - - context 'when the primary key is specified' do - context 'when the primary_key is :id' do - let :primary_key do - :id - end - - let :columns do - [ - mock_column(:id, :integer), - mock_column(:name, :string, limit: 50) - ] - end - - context 'when option "format_rdoc" is true' do - let :options do - { format_rdoc: true } - end - - let :expected_result do - <<~EOS - # == Schema Information - # - # Table name: users - # - # *id*:: integer, not null, primary key - # *name*:: string(50), not null - #-- - # == Schema Information End - #++ - EOS - end - - it 'returns schema info in RDoc format' do - is_expected.to eq(expected_result) - end - end - - context 'when option "format_yard" is true' do - let :options do - { format_yard: true } - end - - let :expected_result do - <<~EOS - # == Schema Information - # - # Table name: users - # - # @!attribute id - # @return [Integer] - # @!attribute name - # @return [String] - # - EOS - end - - it 'returns schema info in YARD format' do - is_expected.to eq(expected_result) - end - end - - context 'when option "format_markdown" is true' do - context 'when other option is not specified' do - let :options do - { format_markdown: true } - end - - let :expected_result do - <<~EOS - # == Schema Information - # - # Table name: `users` - # - # ### Columns - # - # Name | Type | Attributes - # ----------- | ------------------ | --------------------------- - # **`id`** | `integer` | `not null, primary key` - # **`name`** | `string(50)` | `not null` - # - EOS - end - - it 'returns schema info in Markdown format' do - is_expected.to eq(expected_result) - end - end - - context 'when option "show_indexes" is true' do - let :options do - { format_markdown: true, show_indexes: true } - end - - context 'when indexes are normal' do - let :indexes do - [ - mock_index('index_rails_02e851e3b7', columns: ['id']), - mock_index('index_rails_02e851e3b8', columns: ['foreign_thing_id']) - ] - end - - let :expected_result do - <<~EOS - # == Schema Information - # - # Table name: `users` - # - # ### Columns - # - # Name | Type | Attributes - # ----------- | ------------------ | --------------------------- - # **`id`** | `integer` | `not null, primary key` - # **`name`** | `string(50)` | `not null` - # - # ### Indexes - # - # * `index_rails_02e851e3b7`: - # * **`id`** - # * `index_rails_02e851e3b8`: - # * **`foreign_thing_id`** - # - EOS - end - - it 'returns schema info with index information in Markdown format' do - is_expected.to eq expected_result - end - end - - context 'when one of indexes includes "unique" clause' do - let :indexes do - [ - mock_index('index_rails_02e851e3b7', columns: ['id']), - mock_index('index_rails_02e851e3b8', - columns: ['foreign_thing_id'], - unique: true) - ] - end - - let :expected_result do - <<~EOS - # == Schema Information - # - # Table name: `users` - # - # ### Columns - # - # Name | Type | Attributes - # ----------- | ------------------ | --------------------------- - # **`id`** | `integer` | `not null, primary key` - # **`name`** | `string(50)` | `not null` - # - # ### Indexes - # - # * `index_rails_02e851e3b7`: - # * **`id`** - # * `index_rails_02e851e3b8` (_unique_): - # * **`foreign_thing_id`** - # - EOS - end - - it 'returns schema info with index information in Markdown format' do - is_expected.to eq expected_result - end - end - - context 'when one of indexes includes orderd index key' do - let :indexes do - [ - mock_index('index_rails_02e851e3b7', columns: ['id']), - mock_index('index_rails_02e851e3b8', - columns: ['foreign_thing_id'], - orders: { 'foreign_thing_id' => :desc }) - ] - end - - let :expected_result do - <<~EOS - # == Schema Information - # - # Table name: `users` - # - # ### Columns - # - # Name | Type | Attributes - # ----------- | ------------------ | --------------------------- - # **`id`** | `integer` | `not null, primary key` - # **`name`** | `string(50)` | `not null` - # - # ### Indexes - # - # * `index_rails_02e851e3b7`: - # * **`id`** - # * `index_rails_02e851e3b8`: - # * **`foreign_thing_id DESC`** - # - EOS - end - - it 'returns schema info with index information in Markdown format' do - is_expected.to eq expected_result - end - end - - context 'when one of indexes includes "where" clause and "unique" clause' do - let :indexes do - [ - mock_index('index_rails_02e851e3b7', columns: ['id']), - mock_index('index_rails_02e851e3b8', - columns: ['foreign_thing_id'], - unique: true, - where: 'name IS NOT NULL') - ] - end - - let :expected_result do - <<~EOS - # == Schema Information - # - # Table name: `users` - # - # ### Columns - # - # Name | Type | Attributes - # ----------- | ------------------ | --------------------------- - # **`id`** | `integer` | `not null, primary key` - # **`name`** | `string(50)` | `not null` - # - # ### Indexes - # - # * `index_rails_02e851e3b7`: - # * **`id`** - # * `index_rails_02e851e3b8` (_unique_ _where_ name IS NOT NULL): - # * **`foreign_thing_id`** - # - EOS - end - - it 'returns schema info with index information in Markdown format' do - is_expected.to eq expected_result - end - end - - context 'when one of indexes includes "using" clause other than "btree"' do - let :indexes do - [ - mock_index('index_rails_02e851e3b7', columns: ['id']), - mock_index('index_rails_02e851e3b8', - columns: ['foreign_thing_id'], - using: 'hash') - ] - end - - let :expected_result do - <<~EOS - # == Schema Information - # - # Table name: `users` - # - # ### Columns - # - # Name | Type | Attributes - # ----------- | ------------------ | --------------------------- - # **`id`** | `integer` | `not null, primary key` - # **`name`** | `string(50)` | `not null` - # - # ### Indexes - # - # * `index_rails_02e851e3b7`: - # * **`id`** - # * `index_rails_02e851e3b8` (_using_ hash): - # * **`foreign_thing_id`** - # - EOS - end - - it 'returns schema info with index information in Markdown format' do - is_expected.to eq expected_result - end - end - end - - context 'when option "show_foreign_keys" is true' do - let :options do - { format_markdown: true, show_foreign_keys: true } - end - - let :columns do - [ - mock_column(:id, :integer), - mock_column(:foreign_thing_id, :integer) - ] - end - - context 'when foreign_keys have option "on_delete" and "on_update"' do - let :foreign_keys do - [ - mock_foreign_key('fk_rails_02e851e3b7', - 'foreign_thing_id', - 'foreign_things', - 'id', - on_delete: 'on_delete_value', - on_update: 'on_update_value') - ] - end - - let :expected_result do - <<~EOS - # == Schema Information - # - # Table name: `users` - # - # ### Columns - # - # Name | Type | Attributes - # ----------------------- | ------------------ | --------------------------- - # **`id`** | `integer` | `not null, primary key` - # **`foreign_thing_id`** | `integer` | `not null` - # - # ### Foreign Keys - # - # * `fk_rails_...` (_ON DELETE => on_delete_value ON UPDATE => on_update_value_): - # * **`foreign_thing_id => foreign_things.id`** - # - EOS - end - - it 'returns schema info with foreign_keys in Markdown format' do - is_expected.to eq(expected_result) - end - end - end - end - - context 'when "format_doc" and "with_comment" are specified in options' do - let :options do - { format_rdoc: true, with_comment: true } - end - - context 'when columns are normal' do - let :columns do - [ - mock_column(:id, :integer, comment: 'ID'), - mock_column(:name, :string, limit: 50, comment: 'Name') - ] - end - - let :expected_result do - <<~EOS - # == Schema Information - # - # Table name: users - # - # *id(ID)*:: integer, not null, primary key - # *name(Name)*:: string(50), not null - #-- - # == Schema Information End - #++ - EOS - end - - it 'returns schema info in RDoc format' do - is_expected.to eq expected_result - end - end - end - - context 'when "format_markdown" and "with_comment" are specified in options' do - let :options do - { format_markdown: true, with_comment: true } - end - - context 'when columns have comments' do - let :columns do - [ - mock_column(:id, :integer, comment: 'ID'), - mock_column(:name, :string, limit: 50, comment: 'Name') - ] - end - - let :expected_result do - <<~EOS - # == Schema Information - # - # Table name: `users` - # - # ### Columns - # - # Name | Type | Attributes - # ----------------- | ------------------ | --------------------------- - # **`id(ID)`** | `integer` | `not null, primary key` - # **`name(Name)`** | `string(50)` | `not null` - # - EOS - end - - it 'returns schema info in Markdown format' do - is_expected.to eq expected_result - end - end - - context 'when columns have multibyte comments' do - let :columns do - [ - mock_column(:id, :integer, comment: 'ID'), - mock_column(:name, :string, limit: 50, comment: 'NAME') - ] - end - - let :expected_result do - <<~EOS - # == Schema Information - # - # Table name: `users` - # - # ### Columns - # - # Name | Type | Attributes - # --------------------- | ------------------ | --------------------------- - # **`id(ID)`** | `integer` | `not null, primary key` - # **`name(NAME)`** | `string(50)` | `not null` - # - EOS - end - - it 'returns schema info in Markdown format' do - is_expected.to eq expected_result - end - end - end - end - end - end - end - end - - describe '.set_defaults' do - subject do - Annotate::Helpers.true?(ENV['show_complete_foreign_keys']) - end - - context 'when default value of "show_complete_foreign_keys" is not set' do - it 'returns false' do - is_expected.to be(false) - end - end - - context 'when default value of "show_complete_foreign_keys" is set' do - before do - Annotate.set_defaults('show_complete_foreign_keys' => 'true') - end - - it 'returns true' do - is_expected.to be(true) - end - end - - after :each do - ENV.delete('show_complete_foreign_keys') - end - end - - describe '.get_patterns' do - subject { AnnotateModels.get_patterns(options, pattern_type) } - - context 'when pattern_type is "additional_file_patterns"' do - let(:pattern_type) { 'additional_file_patterns' } - - context 'when additional_file_patterns is specified in the options' do - let(:additional_file_patterns) do - [ - '/%PLURALIZED_MODEL_NAME%/**/*.rb', - '/bar/%PLURALIZED_MODEL_NAME%/*_form' - ] - end - - let(:options) { { additional_file_patterns: additional_file_patterns } } - - it 'returns additional_file_patterns in the argument "options"' do - is_expected.to eq(additional_file_patterns) - end - end - - context 'when additional_file_patterns is not specified in the options' do - let(:options) { {} } - - it 'returns an empty array' do - is_expected.to eq([]) - end - end - end - end - - describe '.get_model_files' do - subject { described_class.get_model_files(options) } - - before do - ARGV.clear - - described_class.model_dir = [model_dir] - end - - context 'when `model_dir` is valid' do - let(:model_dir) do - Files do - file 'foo.rb' - dir 'bar' do - file 'baz.rb' - dir 'qux' do - file 'quux.rb' - end - end - dir 'concerns' do - file 'corge.rb' - end - end - end - - context 'when the model files are not specified' do - context 'when no option is specified' do - let(:options) { {} } - - it 'returns all model files under `model_dir` directory' do - is_expected.to contain_exactly( - [model_dir, 'foo.rb'], - [model_dir, File.join('bar', 'baz.rb')], - [model_dir, File.join('bar', 'qux', 'quux.rb')] - ) - end - end - - context 'when `ignore_model_sub_dir` option is enabled' do - let(:options) { { ignore_model_sub_dir: true } } - - it 'returns model files just below `model_dir` directory' do - is_expected.to contain_exactly([model_dir, 'foo.rb']) - end - end - end - - context 'when the model files are specified' do - let(:additional_model_dir) { 'additional_model' } - let(:model_files) do - [ - File.join(model_dir, 'foo.rb'), - "./#{File.join(additional_model_dir, 'corge/grault.rb')}" # Specification by relative path - ] - end - - before { ARGV.concat(model_files) } - - context 'when no option is specified' do - let(:options) { {} } - - context 'when all the specified files are in `model_dir` directory' do - before do - described_class.model_dir << additional_model_dir - end - - it 'returns specified files' do - is_expected.to contain_exactly( - [model_dir, 'foo.rb'], - [additional_model_dir, 'corge/grault.rb'] - ) - end - end - - context 'when a model file outside `model_dir` directory is specified' do - it 'exits with the status code' do - begin - subject - raise - rescue SystemExit => e - expect(e.status).to eq(1) - end - end - end - end - - context 'when `is_rake` option is enabled' do - let(:options) { { is_rake: true } } - - it 'returns all model files under `model_dir` directory' do - is_expected.to contain_exactly( - [model_dir, 'foo.rb'], - [model_dir, File.join('bar', 'baz.rb')], - [model_dir, File.join('bar', 'qux', 'quux.rb')] - ) - end - end - end - end - - context 'when `model_dir` is invalid' do - let(:model_dir) { '/not_exist_path' } - let(:options) { {} } - - it 'exits with the status code' do - begin - subject - raise - rescue SystemExit => e - expect(e.status).to eq(1) - end - end - end - end - - describe '.get_model_class' do - before :all do - AnnotateModels.model_dir = Dir.mktmpdir('annotate_models') - end - - # TODO: use 'files' gem instead - def create(filename, file_content) - File.join(AnnotateModels.model_dir[0], filename).tap do |path| - FileUtils.mkdir_p(File.dirname(path)) - File.open(path, 'wb') do |f| - f.puts(file_content) - end - end - end - - before :each do - create(filename, file_content) - end - - let :klass do - AnnotateModels.get_model_class(File.join(AnnotateModels.model_dir[0], filename)) - end - - context 'when class Foo is defined in "foo.rb"' do - let :filename do - 'foo.rb' - end - - let :file_content do - <<~EOS - class Foo < ActiveRecord::Base - end - EOS - end - - it 'works' do - expect(klass.name).to eq('Foo') - end - end - - context 'when class name is not capitalized normally' do - context 'when class FooWithCAPITALS is defined in "foo_with_capitals.rb"' do - let :filename do - 'foo_with_capitals.rb' - end - - let :file_content do - <<~EOS - class FooWithCAPITALS < ActiveRecord::Base - end - EOS - end - - it 'works' do - expect(klass.name).to eq('FooWithCAPITALS') - end - end - end - - context 'when class is defined inside module' do - context 'when class Bar::FooInsideBar is defined in "bar/foo_inside_bar.rb"' do - let :filename do - 'bar/foo_inside_bar.rb' - end - - let :file_content do - <<~EOS - module Bar - class FooInsideBar < ActiveRecord::Base - end - end - EOS - end - - it 'works' do - expect(klass.name).to eq('Bar::FooInsideBar') - end - end - end - - context 'when class is defined inside module and class name is not capitalized normally' do - context 'when class Bar::FooInsideCapitalsBAR is defined in "bar/foo_inside_capitals_bar.rb"' do - let :filename do - 'bar/foo_inside_capitals_bar.rb' - end - - let :file_content do - <<~EOS - module BAR - class FooInsideCapitalsBAR < ActiveRecord::Base - end - end - EOS - end - - it 'works' do - expect(klass.name).to eq('BAR::FooInsideCapitalsBAR') - end - end - end - - context 'when unknown macros exist in class' do - context 'when class FooWithMacro is defined in "foo_with_macro.rb"' do - let :filename do - 'foo_with_macro.rb' - end - - let :file_content do - <<~EOS - class FooWithMacro < ActiveRecord::Base - acts_as_awesome :yah - end - EOS - end - - it 'works and does not care about known macros' do - expect(klass.name).to eq('FooWithMacro') - end - end - - context 'when class name is with ALL CAPS segments' do - context 'when class is "FooWithCAPITALS" is defined in "foo_with_capitals.rb"' do - let :filename do - 'foo_with_capitals.rb' - end - - let :file_content do - <<~EOS - class FooWithCAPITALS < ActiveRecord::Base - acts_as_awesome :yah - end - EOS - end - - it 'works' do - expect(klass.name).to eq('FooWithCAPITALS') - end - end - end - end - - context 'when known macros exist in class' do - context 'when class FooWithKnownMacro is defined in "foo_with_known_macro.rb"' do - let :filename do - 'foo_with_known_macro.rb' - end - - let :file_content do - <<~EOS - class FooWithKnownMacro < ActiveRecord::Base - has_many :yah - end - EOS - end - - it 'works and does not care about known macros' do - expect(klass.name).to eq('FooWithKnownMacro') - end - end - end - - context 'when the file includes invalid multibyte chars (USASCII)' do - context 'when class FooWithUtf8 is defined in "foo_with_utf8.rb"' do - let :filename do - 'foo_with_utf8.rb' - end - - let :file_content do - <<~EOS - # encoding: utf-8 - class FooWithUtf8 < ActiveRecord::Base - UTF8STRINGS = %w[résumé façon âge] - end - EOS - end - - it 'works without complaining of invalid multibyte chars' do - expect(klass.name).to eq('FooWithUtf8') - end - end - end - - context 'when non-namespaced model is inside subdirectory' do - context 'when class NonNamespacedFooInsideBar is defined in "bar/non_namespaced_foo_inside_bar.rb"' do - let :filename do - 'bar/non_namespaced_foo_inside_bar.rb' - end - - let :file_content do - <<~EOS - class NonNamespacedFooInsideBar < ActiveRecord::Base - end - EOS - end - - it 'works' do - expect(klass.name).to eq('NonNamespacedFooInsideBar') - end - end - - context 'when class name is not capitalized normally' do - context 'when class NonNamespacedFooWithCapitalsInsideBar is defined in "bar/non_namespaced_foo_with_capitals_inside_bar.rb"' do - let :filename do - 'bar/non_namespaced_foo_with_capitals_inside_bar.rb' - end - - let :file_content do - <<~EOS - class NonNamespacedFooWithCapitalsInsideBar < ActiveRecord::Base - end - EOS - end - - it 'works' do - expect(klass.name).to eq('NonNamespacedFooWithCapitalsInsideBar') - end - end - end - end - - context 'when class file is loaded twice' do - context 'when class LoadedClass is defined in "loaded_class.rb"' do - let :filename do - 'loaded_class.rb' - end - - let :file_content do - <<~EOS - class LoadedClass < ActiveRecord::Base - CONSTANT = 1 - end - EOS - end - - before :each do - path = File.expand_path(filename, AnnotateModels.model_dir[0]) - Kernel.load(path) - expect(Kernel).not_to receive(:require) - end - - it 'does not require model file twice' do - expect(klass.name).to eq('LoadedClass') - end - end - - context 'when class is defined in a subdirectory' do - dir = Array.new(8) { (0..9).to_a.sample(random: Random.new) }.join - - context "when class SubdirLoadedClass is defined in \"#{dir}/subdir_loaded_class.rb\"" do - before :each do - $LOAD_PATH.unshift(File.join(AnnotateModels.model_dir[0], dir)) - - path = File.expand_path(filename, AnnotateModels.model_dir[0]) - Kernel.load(path) - expect(Kernel).not_to receive(:require) - end - - let :filename do - "#{dir}/subdir_loaded_class.rb" - end - - let :file_content do - <<~EOS - class SubdirLoadedClass < ActiveRecord::Base - CONSTANT = 1 - end - EOS - end - - it 'does not require model file twice' do - expect(klass.name).to eq('SubdirLoadedClass') - end - end - end - end - - context 'when two class exist' do - before :each do - create(filename_2, file_content_2) - end - - context 'the base names are duplicated' do - let :filename do - 'foo.rb' - end - - let :file_content do - <<-EOS - class Foo < ActiveRecord::Base - end - EOS - end - - let :filename_2 do - 'bar/foo.rb' - end - - let :file_content_2 do - <<-EOS - class Bar::Foo < ActiveRecord::Base - end - EOS - end - - let :klass_2 do - AnnotateModels.get_model_class(File.join(AnnotateModels.model_dir[0], filename_2)) - end - - it 'finds valid model' do - expect(klass.name).to eq('Foo') - expect(klass_2.name).to eq('Bar::Foo') - end - end - - context 'the class name and base name clash' do - let :filename do - 'foo.rb' - end - - let :file_content do - <<-EOS - class Foo < ActiveRecord::Base - end - EOS - end - - let :filename_2 do - 'bar/foo.rb' - end - - let :file_content_2 do - <<-EOS - class Bar::Foo < ActiveRecord::Base - end - EOS - end - - let :klass_2 do - AnnotateModels.get_model_class(File.join(AnnotateModels.model_dir[0], filename_2)) - end - - it 'finds valid model' do - expect(klass.name).to eq('Foo') - expect(klass_2.name).to eq('Bar::Foo') - end - - it 'attempts to load the model path without expanding if skip_subdirectory_model_load is false' do - allow(AnnotateModels).to receive(:skip_subdirectory_model_load).and_return(false) - full_path = File.join(AnnotateModels.model_dir[0], filename_2) - expect(File).to_not receive(:expand_path).with(full_path) - AnnotateModels.get_model_class(full_path) - end - - it 'does not attempt to load the model path without expanding if skip_subdirectory_model_load is true' do - $LOAD_PATH.unshift(AnnotateModels.model_dir[0]) - allow(AnnotateModels).to receive(:skip_subdirectory_model_load).and_return(true) - full_path = File.join(AnnotateModels.model_dir[0], filename_2) - expect(File).to receive(:expand_path).with(full_path).and_call_original - AnnotateModels.get_model_class(full_path) - end - end - - context 'one of the classes is nested in another class' do - let :filename do - 'voucher.rb' - end - - let :file_content do - <<-EOS - class Voucher < ActiveRecord::Base - end - EOS - end - - let :filename_2 do - 'voucher/foo.rb' - end - - let :file_content_2 do - <<~EOS - class Voucher - class Foo < ActiveRecord::Base - end - end - EOS - end - - let :klass_2 do - AnnotateModels.get_model_class(File.join(AnnotateModels.model_dir[0], filename_2)) - end - - it 'finds valid model' do - expect(klass.name).to eq('Voucher') - expect(klass_2.name).to eq('Voucher::Foo') - end - end - end - end - - describe '.remove_annotation_of_file' do - subject do - AnnotateModels.remove_annotation_of_file(path) - end - - let :tmpdir do - Dir.mktmpdir('annotate_models') - end - - let :path do - File.join(tmpdir, filename).tap do |path| - File.open(path, 'w') do |f| - f.puts(file_content) - end - end - end - - let :file_content_after_removal do - subject - File.read(path) - end - - let :expected_result do - <<~EOS - class Foo < ActiveRecord::Base - end - EOS - end - - context 'when annotation is before main content' do - let :filename do - 'before.rb' - end - - let :file_content do - <<~EOS - # == Schema Information - # - # Table name: foo - # - # id :integer not null, primary key - # created_at :datetime - # updated_at :datetime - # - - class Foo < ActiveRecord::Base - end - EOS - end - - it 'removes annotation' do - expect(file_content_after_removal).to eq expected_result - end - end - - context 'when annotation is before main content and CRLF is used for line breaks' do - let :filename do - 'before.rb' - end - - let :file_content do - <<~EOS - # == Schema Information - # - # Table name: foo\r\n# - # id :integer not null, primary key - # created_at :datetime - # updated_at :datetime - # - \r\n - class Foo < ActiveRecord::Base - end - EOS - end - - it 'removes annotation' do - expect(file_content_after_removal).to eq expected_result - end - end - - context 'when annotation is before main content and with opening wrapper' do - let :filename do - 'opening_wrapper.rb' - end - - let :file_content do - <<~EOS - # wrapper - # == Schema Information - # - # Table name: foo - # - # id :integer not null, primary key - # created_at :datetime - # updated_at :datetime - # - - class Foo < ActiveRecord::Base - end - EOS - end - - subject do - AnnotateModels.remove_annotation_of_file(path, wrapper_open: 'wrapper') - end - - it 'removes annotation' do - expect(file_content_after_removal).to eq expected_result - end - end - - context 'when annotation is before main content and with opening wrapper' do - let :filename do - 'opening_wrapper.rb' - end - - let :file_content do - <<~EOS - # wrapper\r\n# == Schema Information - # - # Table name: foo - # - # id :integer not null, primary key - # created_at :datetime - # updated_at :datetime - # - - class Foo < ActiveRecord::Base - end - EOS - end - - subject do - AnnotateModels.remove_annotation_of_file(path, wrapper_open: 'wrapper') - end - - it 'removes annotation' do - expect(file_content_after_removal).to eq expected_result - end - end - - context 'when annotation is after main content' do - let :filename do - 'after.rb' - end +MAGIC_COMMENTS = [ + '# encoding: UTF-8', + '# coding: UTF-8', + '# -*- coding: UTF-8 -*-', + '#encoding: utf-8', + '# encoding: utf-8', + '# -*- encoding : utf-8 -*-', + "# encoding: utf-8\n# frozen_string_literal: true", + "# frozen_string_literal: true\n# encoding: utf-8", + '# frozen_string_literal: true', + '#frozen_string_literal: false', + '# -*- frozen_string_literal : true -*-' +].freeze - let :file_content do - <<~EOS - class Foo < ActiveRecord::Base - end - - # == Schema Information - # - # Table name: foo - # - # id :integer not null, primary key - # created_at :datetime - # updated_at :datetime - # - - EOS - end - - it 'removes annotation' do - expect(file_content_after_removal).to eq expected_result - end - end - - context 'when annotation is after main content and with closing wrapper' do - let :filename do - 'closing_wrapper.rb' - end - - let :file_content do - <<~EOS - class Foo < ActiveRecord::Base - end - - # == Schema Information - # - # Table name: foo - # - # id :integer not null, primary key - # created_at :datetime - # updated_at :datetime - # - # wrapper - - EOS - end - - subject do - AnnotateModels.remove_annotation_of_file(path, wrapper_close: 'wrapper') - end - - it 'removes annotation' do - expect(file_content_after_removal).to eq expected_result - end - end - - context 'when annotation is before main content and with comment "-*- SkipSchemaAnnotations"' do - let :filename do - 'skip.rb' - end - - let :file_content do - <<~EOS - # -*- SkipSchemaAnnotations - # == Schema Information - # - # Table name: foo - # - # id :integer not null, primary key - # created_at :datetime - # updated_at :datetime - # - - class Foo < ActiveRecord::Base - end - EOS - end - - let :expected_result do - file_content - end - - it 'does not remove annotation' do - expect(file_content_after_removal).to eq expected_result - end - end - end - - describe '.resolve_filename' do - subject do - AnnotateModels.resolve_filename(filename_template, model_name, table_name) - end - - context 'When model_name is "example_model" and table_name is "example_models"' do - let(:model_name) { 'example_model' } - let(:table_name) { 'example_models' } - - context "when filename_template is 'test/unit/%MODEL_NAME%_test.rb'" do - let(:filename_template) { 'test/unit/%MODEL_NAME%_test.rb' } - - it 'returns the test path for a model' do - is_expected.to eq 'test/unit/example_model_test.rb' - end - end - - context "when filename_template is '/foo/bar/%MODEL_NAME%/testing.rb'" do - let(:filename_template) { '/foo/bar/%MODEL_NAME%/testing.rb' } - - it 'returns the additional glob' do - is_expected.to eq '/foo/bar/example_model/testing.rb' - end - end - - context "when filename_template is '/foo/bar/%PLURALIZED_MODEL_NAME%/testing.rb'" do - let(:filename_template) { '/foo/bar/%PLURALIZED_MODEL_NAME%/testing.rb' } - - it 'returns the additional glob' do - is_expected.to eq '/foo/bar/example_models/testing.rb' - end - end - - context "when filename_template is 'test/fixtures/%TABLE_NAME%.yml'" do - let(:filename_template) { 'test/fixtures/%TABLE_NAME%.yml' } - - it 'returns the fixture path for a model' do - is_expected.to eq 'test/fixtures/example_models.yml' - end - end - end - - context 'When model_name is "parent/child" and table_name is "parent_children"' do - let(:model_name) { 'parent/child' } - let(:table_name) { 'parent_children' } - - context "when filename_template is 'test/fixtures/%PLURALIZED_MODEL_NAME%.yml'" do - let(:filename_template) { 'test/fixtures/%PLURALIZED_MODEL_NAME%.yml' } - - it 'returns the fixture path for a nested model' do - is_expected.to eq 'test/fixtures/parent/children.yml' - end - end - end - end - - describe 'annotating a file' do +describe AnnotateModels do + describe 'when annotate a file' do before do @model_dir = Dir.mktmpdir('annotate_models') (@model_file_name, @file_content) = write_model 'user.rb', <<~EOS @@ -2724,7 +249,7 @@ class User < ActiveRecord::Base end end - describe "if a file can't be annotated" do + describe "that can't be annotated" do before do allow(AnnotateModels).to receive(:get_loaded_model_by_path).with('user').and_return(nil) @@ -2735,18 +260,18 @@ class User < ActiveRecord::Base EOS end - it 'displays just the error message with trace disabled (default)' do + it 'should display just the error message with trace disabled (default)' do expect { AnnotateModels.do_annotations model_dir: @model_dir, is_rake: true }.to output(a_string_including("Unable to annotate #{@model_dir}/user.rb: oops")).to_stderr expect { AnnotateModels.do_annotations model_dir: @model_dir, is_rake: true }.not_to output(a_string_including('/spec/annotate/annotate_models_spec.rb:')).to_stderr end - it 'displays the error message and stacktrace with trace enabled' do + it 'should display the error message and stacktrace with trace enabled' do expect { AnnotateModels.do_annotations model_dir: @model_dir, is_rake: true, trace: true }.to output(a_string_including("Unable to annotate #{@model_dir}/user.rb: oops")).to_stderr expect { AnnotateModels.do_annotations model_dir: @model_dir, is_rake: true, trace: true }.to output(a_string_including('/spec/lib/annotate/annotate_models_spec.rb:')).to_stderr end end - describe "if a file can't be deannotated" do + describe "that can't be de-annotated" do before do allow(AnnotateModels).to receive(:get_loaded_model_by_path).with('user').and_return(nil) @@ -2757,12 +282,12 @@ class User < ActiveRecord::Base EOS end - it 'displays just the error message with trace disabled (default)' do + it 'should display just the error message with trace disabled (default)' do expect { AnnotateModels.remove_annotations model_dir: @model_dir, is_rake: true }.to output(a_string_including("Unable to deannotate #{@model_dir}/user.rb: oops")).to_stderr expect { AnnotateModels.remove_annotations model_dir: @model_dir, is_rake: true }.not_to output(a_string_including("/user.rb:2:in `'")).to_stderr end - it 'displays the error message and stacktrace with trace enabled' do + it 'should display the error message and stacktrace with trace enabled' do expect { AnnotateModels.remove_annotations model_dir: @model_dir, is_rake: true, trace: true }.to output(a_string_including("Unable to deannotate #{@model_dir}/user.rb: oops")).to_stderr expect { AnnotateModels.remove_annotations model_dir: @model_dir, is_rake: true, trace: true }.to output(a_string_including("/user.rb:2:in `'")).to_stderr end diff --git a/spec/lib/annotate/annotate_models/file_patterns_spec.rb b/spec/lib/annotate/models/file_patterns_spec.rb similarity index 100% rename from spec/lib/annotate/annotate_models/file_patterns_spec.rb rename to spec/lib/annotate/models/file_patterns_spec.rb diff --git a/spec/lib/annotate/models/get_model_class_spec.rb b/spec/lib/annotate/models/get_model_class_spec.rb new file mode 100644 index 000000000..53f83a074 --- /dev/null +++ b/spec/lib/annotate/models/get_model_class_spec.rb @@ -0,0 +1,408 @@ +require_relative '../../../spec_helper' +require_relative 'model_spec_helper' +require 'annotate/annotate_models' +require 'annotate/active_record_patch' +require 'active_support/core_ext/string' +require 'files' +require 'tmpdir' + +describe AnnotateModels do + describe '.get_model_class' do + before :all do + AnnotateModels.model_dir = Dir.mktmpdir('annotate_models') + end + + # TODO: use 'files' gem instead + def create(filename, file_content) + File.join(AnnotateModels.model_dir[0], filename).tap do |path| + FileUtils.mkdir_p(File.dirname(path)) + File.open(path, 'wb') do |f| + f.puts(file_content) + end + end + end + + before :each do + create(filename, file_content) + end + + let :klass do + AnnotateModels.get_model_class(File.join(AnnotateModels.model_dir[0], filename)) + end + + context 'when class Foo is defined in "foo.rb"' do + let :filename do + 'foo.rb' + end + + let :file_content do + <<~EOS + class Foo < ActiveRecord::Base + end + EOS + end + + it 'works' do + expect(klass.name).to eq('Foo') + end + end + + context 'when class name is not capitalized normally' do + context 'when class FooWithCAPITALS is defined in "foo_with_capitals.rb"' do + let :filename do + 'foo_with_capitals.rb' + end + + let :file_content do + <<~EOS + class FooWithCAPITALS < ActiveRecord::Base + end + EOS + end + + it 'works' do + expect(klass.name).to eq('FooWithCAPITALS') + end + end + end + + context 'when class is defined inside module' do + context 'when class Bar::FooInsideBar is defined in "bar/foo_inside_bar.rb"' do + let :filename do + 'bar/foo_inside_bar.rb' + end + + let :file_content do + <<~EOS + module Bar + class FooInsideBar < ActiveRecord::Base + end + end + EOS + end + + it 'works' do + expect(klass.name).to eq('Bar::FooInsideBar') + end + end + end + + context 'when class is defined inside module and class name is not capitalized normally' do + context 'when class Bar::FooInsideCapitalsBAR is defined in "bar/foo_inside_capitals_bar.rb"' do + let :filename do + 'bar/foo_inside_capitals_bar.rb' + end + + let :file_content do + <<~EOS + module BAR + class FooInsideCapitalsBAR < ActiveRecord::Base + end + end + EOS + end + + it 'works' do + expect(klass.name).to eq('BAR::FooInsideCapitalsBAR') + end + end + end + + context 'when unknown macros exist in class' do + context 'when class FooWithMacro is defined in "foo_with_macro.rb"' do + let :filename do + 'foo_with_macro.rb' + end + + let :file_content do + <<~EOS + class FooWithMacro < ActiveRecord::Base + acts_as_awesome :yah + end + EOS + end + + it 'works and does not care about known macros' do + expect(klass.name).to eq('FooWithMacro') + end + end + + context 'when class name is with ALL CAPS segments' do + context 'when class is "FooWithCAPITALS" is defined in "foo_with_capitals.rb"' do + let :filename do + 'foo_with_capitals.rb' + end + + let :file_content do + <<~EOS + class FooWithCAPITALS < ActiveRecord::Base + acts_as_awesome :yah + end + EOS + end + + it 'works' do + expect(klass.name).to eq('FooWithCAPITALS') + end + end + end + end + + context 'when known macros exist in class' do + context 'when class FooWithKnownMacro is defined in "foo_with_known_macro.rb"' do + let :filename do + 'foo_with_known_macro.rb' + end + + let :file_content do + <<~EOS + class FooWithKnownMacro < ActiveRecord::Base + has_many :yah + end + EOS + end + + it 'works and does not care about known macros' do + expect(klass.name).to eq('FooWithKnownMacro') + end + end + end + + context 'when the file includes invalid multibyte chars (USASCII)' do + context 'when class FooWithUtf8 is defined in "foo_with_utf8.rb"' do + let :filename do + 'foo_with_utf8.rb' + end + + let :file_content do + <<~EOS + # encoding: utf-8 + class FooWithUtf8 < ActiveRecord::Base + UTF8STRINGS = %w[résumé façon âge] + end + EOS + end + + it 'works without complaining of invalid multibyte chars' do + expect(klass.name).to eq('FooWithUtf8') + end + end + end + + context 'when non-namespaced model is inside subdirectory' do + context 'when class NonNamespacedFooInsideBar is defined in "bar/non_namespaced_foo_inside_bar.rb"' do + let :filename do + 'bar/non_namespaced_foo_inside_bar.rb' + end + + let :file_content do + <<~EOS + class NonNamespacedFooInsideBar < ActiveRecord::Base + end + EOS + end + + it 'works' do + expect(klass.name).to eq('NonNamespacedFooInsideBar') + end + end + + context 'when class name is not capitalized normally' do + context 'when class NonNamespacedFooWithCapitalsInsideBar is defined in "bar/non_namespaced_foo_with_capitals_inside_bar.rb"' do + let :filename do + 'bar/non_namespaced_foo_with_capitals_inside_bar.rb' + end + + let :file_content do + <<~EOS + class NonNamespacedFooWithCapitalsInsideBar < ActiveRecord::Base + end + EOS + end + + it 'works' do + expect(klass.name).to eq('NonNamespacedFooWithCapitalsInsideBar') + end + end + end + end + + context 'when class file is loaded twice' do + context 'when class LoadedClass is defined in "loaded_class.rb"' do + let :filename do + 'loaded_class.rb' + end + + let :file_content do + <<~EOS + class LoadedClass < ActiveRecord::Base + CONSTANT = 1 + end + EOS + end + + before :each do + path = File.expand_path(filename, AnnotateModels.model_dir[0]) + Kernel.load(path) + expect(Kernel).not_to receive(:require) + end + + it 'does not require model file twice' do + expect(klass.name).to eq('LoadedClass') + end + end + + context 'when class is defined in a subdirectory' do + dir = Array.new(8) { (0..9).to_a.sample(random: Random.new) }.join + + context "when class SubdirLoadedClass is defined in \"#{dir}/subdir_loaded_class.rb\"" do + before :each do + $LOAD_PATH.unshift(File.join(AnnotateModels.model_dir[0], dir)) + + path = File.expand_path(filename, AnnotateModels.model_dir[0]) + Kernel.load(path) + expect(Kernel).not_to receive(:require) + end + + let :filename do + "#{dir}/subdir_loaded_class.rb" + end + + let :file_content do + <<~EOS + class SubdirLoadedClass < ActiveRecord::Base + CONSTANT = 1 + end + EOS + end + + it 'does not require model file twice' do + expect(klass.name).to eq('SubdirLoadedClass') + end + end + end + end + + context 'when two class exist' do + before :each do + create(filename_2, file_content_2) + end + + context 'the base names are duplicated' do + let :filename do + 'foo.rb' + end + + let :file_content do + <<-EOS + class Foo < ActiveRecord::Base + end + EOS + end + + let :filename_2 do + 'bar/foo.rb' + end + + let :file_content_2 do + <<-EOS + class Bar::Foo < ActiveRecord::Base + end + EOS + end + + let :klass_2 do + AnnotateModels.get_model_class(File.join(AnnotateModels.model_dir[0], filename_2)) + end + + it 'finds valid model' do + expect(klass.name).to eq('Foo') + expect(klass_2.name).to eq('Bar::Foo') + end + end + + context 'the class name and base name clash' do + let :filename do + 'foo.rb' + end + + let :file_content do + <<-EOS + class Foo < ActiveRecord::Base + end + EOS + end + + let :filename_2 do + 'bar/foo.rb' + end + + let :file_content_2 do + <<-EOS + class Bar::Foo < ActiveRecord::Base + end + EOS + end + + let :klass_2 do + AnnotateModels.get_model_class(File.join(AnnotateModels.model_dir[0], filename_2)) + end + + it 'finds valid model' do + expect(klass.name).to eq('Foo') + expect(klass_2.name).to eq('Bar::Foo') + end + + it 'attempts to load the model path without expanding if skip_subdirectory_model_load is false' do + allow(AnnotateModels).to receive(:skip_subdirectory_model_load).and_return(false) + full_path = File.join(AnnotateModels.model_dir[0], filename_2) + expect(File).to_not receive(:expand_path).with(full_path) + AnnotateModels.get_model_class(full_path) + end + + it 'does not attempt to load the model path without expanding if skip_subdirectory_model_load is true' do + $LOAD_PATH.unshift(AnnotateModels.model_dir[0]) + allow(AnnotateModels).to receive(:skip_subdirectory_model_load).and_return(true) + full_path = File.join(AnnotateModels.model_dir[0], filename_2) + expect(File).to receive(:expand_path).with(full_path).and_call_original + AnnotateModels.get_model_class(full_path) + end + end + + context 'one of the classes is nested in another class' do + let :filename do + 'voucher.rb' + end + + let :file_content do + <<-EOS + class Voucher < ActiveRecord::Base + end + EOS + end + + let :filename_2 do + 'voucher/foo.rb' + end + + let :file_content_2 do + <<~EOS + class Voucher + class Foo < ActiveRecord::Base + end + end + EOS + end + + let :klass_2 do + AnnotateModels.get_model_class(File.join(AnnotateModels.model_dir[0], filename_2)) + end + + it 'finds valid model' do + expect(klass.name).to eq('Voucher') + expect(klass_2.name).to eq('Voucher::Foo') + end + end + end + end +end diff --git a/spec/lib/annotate/models/get_model_files_spec.rb b/spec/lib/annotate/models/get_model_files_spec.rb new file mode 100644 index 000000000..66857fa1e --- /dev/null +++ b/spec/lib/annotate/models/get_model_files_spec.rb @@ -0,0 +1,120 @@ +require_relative '../../../spec_helper' +require_relative 'model_spec_helper' +require 'annotate/annotate_models' +require 'annotate/active_record_patch' +require 'active_support/core_ext/string' +require 'files' +require 'tmpdir' + +describe AnnotateModels do + describe '.get_model_files' do + subject { described_class.get_model_files(options) } + + before do + ARGV.clear + + described_class.model_dir = [model_dir] + end + + context 'when `model_dir` is valid' do + let(:model_dir) do + Files do + file 'foo.rb' + dir 'bar' do + file 'baz.rb' + dir 'qux' do + file 'quux.rb' + end + end + dir 'concerns' do + file 'corge.rb' + end + end + end + + context 'when the model files are not specified' do + context 'when no option is specified' do + let(:options) { {} } + + it 'returns all model files under `model_dir` directory' do + is_expected.to contain_exactly( + [model_dir, 'foo.rb'], + [model_dir, File.join('bar', 'baz.rb')], + [model_dir, File.join('bar', 'qux', 'quux.rb')] + ) + end + end + + context 'when `ignore_model_sub_dir` option is enabled' do + let(:options) { { ignore_model_sub_dir: true } } + + it 'returns model files just below `model_dir` directory' do + is_expected.to contain_exactly([model_dir, 'foo.rb']) + end + end + end + + context 'when the model files are specified' do + let(:additional_model_dir) { 'additional_model' } + let(:model_files) do + [ + File.join(model_dir, 'foo.rb'), + "./#{File.join(additional_model_dir, 'corge/grault.rb')}" # Specification by relative path + ] + end + + before { ARGV.concat(model_files) } + + context 'when no option is specified' do + let(:options) { {} } + + context 'when all the specified files are in `model_dir` directory' do + before do + described_class.model_dir << additional_model_dir + end + + it 'returns specified files' do + is_expected.to contain_exactly( + [model_dir, 'foo.rb'], + [additional_model_dir, 'corge/grault.rb'] + ) + end + end + + context 'when a model file outside `model_dir` directory is specified' do + it 'exits with the status code' do + subject + raise + rescue SystemExit => e + expect(e.status).to eq(1) + end + end + end + + context 'when `is_rake` option is enabled' do + let(:options) { { is_rake: true } } + + it 'returns all model files under `model_dir` directory' do + is_expected.to contain_exactly( + [model_dir, 'foo.rb'], + [model_dir, File.join('bar', 'baz.rb')], + [model_dir, File.join('bar', 'qux', 'quux.rb')] + ) + end + end + end + end + + context 'when `model_dir` is invalid' do + let(:model_dir) { '/not_exist_path' } + let(:options) { {} } + + it 'exits with the status code' do + subject + raise + rescue SystemExit => e + expect(e.status).to eq(1) + end + end + end +end diff --git a/spec/lib/annotate/models/get_patterns_spec.rb b/spec/lib/annotate/models/get_patterns_spec.rb new file mode 100644 index 000000000..602f93e2b --- /dev/null +++ b/spec/lib/annotate/models/get_patterns_spec.rb @@ -0,0 +1,40 @@ +require_relative '../../../spec_helper' +require_relative 'model_spec_helper' +require 'annotate/annotate_models' +require 'annotate/active_record_patch' +require 'active_support/core_ext/string' +require 'files' +require 'tmpdir' + +describe AnnotateModels do + describe '.get_patterns' do + subject { AnnotateModels.get_patterns(options, pattern_type) } + + context 'when pattern_type is "additional_file_patterns"' do + let(:pattern_type) { 'additional_file_patterns' } + + context 'when additional_file_patterns is specified in the options' do + let(:additional_file_patterns) do + [ + '/%PLURALIZED_MODEL_NAME%/**/*.rb', + '/bar/%PLURALIZED_MODEL_NAME%/*_form' + ] + end + + let(:options) { { additional_file_patterns: additional_file_patterns } } + + it 'returns additional_file_patterns in the argument "options"' do + is_expected.to eq(additional_file_patterns) + end + end + + context 'when additional_file_patterns is not specified in the options' do + let(:options) { {} } + + it 'returns an empty array' do + is_expected.to eq([]) + end + end + end + end +end diff --git a/spec/lib/annotate/models/get_schema_info_spec.rb b/spec/lib/annotate/models/get_schema_info_spec.rb new file mode 100644 index 000000000..577779ff2 --- /dev/null +++ b/spec/lib/annotate/models/get_schema_info_spec.rb @@ -0,0 +1,1436 @@ +require_relative '../../../spec_helper' +require_relative 'model_spec_helper' +require 'annotate/annotate_models' +require 'annotate/active_record_patch' +require 'active_support/core_ext/string' +require 'files' +require 'tmpdir' + +describe AnnotateModels do + describe '.get_schema_info' do + subject do + AnnotateModels.get_schema_info(klass, header, **options) + end + + let :klass do + mock_class(:users, primary_key, columns, indexes, foreign_keys) + end + + let :indexes do + [] + end + + let :foreign_keys do + [] + end + + context 'when option is not present' do + let :options do + {} + end + + context 'when header is "Schema Info"' do + let :header do + 'Schema Info' + end + + context 'when the primary key is not specified' do + let :primary_key do + nil + end + + context 'when the columns are normal' do + let :columns do + [ + mock_column(:id, :integer), + mock_column(:name, :string, limit: 50) + ] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null + # name :string(50) not null + # + EOS + end + + it 'returns schema info' do + is_expected.to eq(expected_result) + end + end + + context 'when an enum column exists' do + let :columns do + [ + mock_column(:id, :integer), + mock_column(:name, :enum, limit: [:enum1, :enum2]) + ] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null + # name :enum not null, (enum1, enum2) + # + EOS + end + + it 'returns schema info' do + is_expected.to eq(expected_result) + end + end + + context 'when unsigned columns exist' do + let :columns do + [ + mock_column(:id, :integer), + mock_column(:integer, :integer, unsigned?: true), + mock_column(:bigint, :integer, unsigned?: true, bigint?: true), + mock_column(:bigint, :bigint, unsigned?: true), + mock_column(:float, :float, unsigned?: true), + mock_column(:decimal, :decimal, unsigned?: true, precision: 10, scale: 2) + ] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null + # integer :integer unsigned, not null + # bigint :bigint unsigned, not null + # bigint :bigint unsigned, not null + # float :float unsigned, not null + # decimal :decimal(10, 2) unsigned, not null + # + EOS + end + + it 'returns schema info' do + is_expected.to eq(expected_result) + end + end + end + + context 'when the primary key is specified' do + context 'when the primary_key is :id' do + let :primary_key do + :id + end + + context 'when columns are normal' do + let :columns do + [ + mock_column(:id, :integer, limit: 8), + mock_column(:name, :string, limit: 50), + mock_column(:notes, :text, limit: 55) + ] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null, primary key + # name :string(50) not null + # notes :text(55) not null + # + EOS + end + + it 'returns schema info' do + is_expected.to eq(expected_result) + end + end + + context 'when columns have default values' do + let :columns do + [ + mock_column(:id, :integer), + mock_column(:size, :integer, default: 20), + mock_column(:flag, :boolean, default: false) + ] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null, primary key + # size :integer default(20), not null + # flag :boolean default(FALSE), not null + # + EOS + end + + it 'returns schema info with default values' do + is_expected.to eq(expected_result) + end + end + + context 'with Globalize gem' do + let :translation_klass do + double('Post::Translation', + to_s: 'Post::Translation', + columns: [ + mock_column(:id, :integer, limit: 8), + mock_column(:post_id, :integer, limit: 8), + mock_column(:locale, :string, limit: 50), + mock_column(:title, :string, limit: 50) + ]) + end + + let :klass do + mock_class(:posts, primary_key, columns, indexes, foreign_keys).tap do |mock_klass| + allow(mock_klass).to receive(:translation_class).and_return(translation_klass) + end + end + + let :columns do + [ + mock_column(:id, :integer, limit: 8), + mock_column(:author_name, :string, limit: 50) + ] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: posts + # + # id :integer not null, primary key + # author_name :string(50) not null + # title :string(50) not null + # + EOS + end + + it 'returns schema info' do + is_expected.to eq expected_result + end + end + end + + context 'when the primary key is an array (using composite_primary_keys)' do + let :primary_key do + [:a_id, :b_id] + end + + let :columns do + [ + mock_column(:a_id, :integer), + mock_column(:b_id, :integer), + mock_column(:name, :string, limit: 50) + ] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # a_id :integer not null, primary key + # b_id :integer not null, primary key + # name :string(50) not null + # + EOS + end + + it 'returns schema info' do + is_expected.to eq(expected_result) + end + end + end + end + end + + context 'when option is present' do + context 'when header is "Schema Info"' do + let :header do + 'Schema Info' + end + + context 'when the primary key is specified' do + context 'when the primary_key is :id' do + let :primary_key do + :id + end + + context 'when indexes exist' do + context 'when option "show_indexes" is true' do + let :options do + { show_indexes: true } + end + + context 'when indexes are normal' do + let :columns do + [ + mock_column(:id, :integer), + mock_column(:foreign_thing_id, :integer) + ] + end + + let :indexes do + [ + mock_index('index_rails_02e851e3b7', columns: ['id']), + mock_index('index_rails_02e851e3b8', columns: ['foreign_thing_id']) + ] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null, primary key + # foreign_thing_id :integer not null + # + # Indexes + # + # index_rails_02e851e3b7 (id) + # index_rails_02e851e3b8 (foreign_thing_id) + # + EOS + end + + it 'returns schema info with index information' do + is_expected.to eq expected_result + end + end + + context 'when one of indexes includes orderd index key' do + let :columns do + [ + mock_column('id', :integer), + mock_column('firstname', :string), + mock_column('surname', :string), + mock_column('value', :string) + ] + end + + let :indexes do + [ + mock_index('index_rails_02e851e3b7', columns: ['id']), + mock_index('index_rails_02e851e3b8', + columns: %w[firstname surname value], + orders: { 'surname' => :asc, 'value' => :desc }) + ] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null, primary key + # firstname :string not null + # surname :string not null + # value :string not null + # + # Indexes + # + # index_rails_02e851e3b7 (id) + # index_rails_02e851e3b8 (firstname,surname ASC,value DESC) + # + EOS + end + + it 'returns schema info with index information' do + is_expected.to eq expected_result + end + end + + context 'when one of indexes includes "where" clause' do + let :columns do + [ + mock_column('id', :integer), + mock_column('firstname', :string), + mock_column('surname', :string), + mock_column('value', :string) + ] + end + + let :indexes do + [ + mock_index('index_rails_02e851e3b7', columns: ['id']), + mock_index('index_rails_02e851e3b8', + columns: %w[firstname surname], + where: 'value IS NOT NULL') + ] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null, primary key + # firstname :string not null + # surname :string not null + # value :string not null + # + # Indexes + # + # index_rails_02e851e3b7 (id) + # index_rails_02e851e3b8 (firstname,surname) WHERE value IS NOT NULL + # + EOS + end + + it 'returns schema info with index information' do + is_expected.to eq expected_result + end + end + + context 'when one of indexes includes "using" clause other than "btree"' do + let :columns do + [ + mock_column('id', :integer), + mock_column('firstname', :string), + mock_column('surname', :string), + mock_column('value', :string) + ] + end + + let :indexes do + [ + mock_index('index_rails_02e851e3b7', columns: ['id']), + mock_index('index_rails_02e851e3b8', + columns: %w[firstname surname], + using: 'hash') + ] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null, primary key + # firstname :string not null + # surname :string not null + # value :string not null + # + # Indexes + # + # index_rails_02e851e3b7 (id) + # index_rails_02e851e3b8 (firstname,surname) USING hash + # + EOS + end + + it 'returns schema info with index information' do + is_expected.to eq expected_result + end + end + + context 'when index is not defined' do + let :columns do + [ + mock_column(:id, :integer), + mock_column(:foreign_thing_id, :integer) + ] + end + + let :indexes do + [] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null, primary key + # foreign_thing_id :integer not null + # + EOS + end + + it 'returns schema info without index information' do + is_expected.to eq expected_result + end + end + end + + context 'when option "simple_indexes" is true' do + let :options do + { simple_indexes: true } + end + + context 'when one of indexes includes "orders" clause' do + let :columns do + [ + mock_column(:id, :integer), + mock_column(:foreign_thing_id, :integer) + ] + end + + let :indexes do + [ + mock_index('index_rails_02e851e3b7', columns: ['id']), + mock_index('index_rails_02e851e3b8', + columns: ['foreign_thing_id'], + orders: { 'foreign_thing_id' => :desc }) + ] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null, primary key + # foreign_thing_id :integer not null + # + EOS + end + + it 'returns schema info with index information' do + is_expected.to eq expected_result + end + end + + context 'when one of indexes is in string form' do + let :columns do + [ + mock_column('id', :integer), + mock_column('name', :string) + ] + end + + let :indexes do + [ + mock_index('index_rails_02e851e3b7', columns: ['id']), + mock_index('index_rails_02e851e3b8', columns: 'LOWER(name)') + ] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null, primary key, indexed + # name :string not null + # + EOS + end + + it 'returns schema info with index information' do + is_expected.to eq expected_result + end + end + end + end + + context 'when foreign keys exist' do + let :columns do + [ + mock_column(:id, :integer), + mock_column(:foreign_thing_id, :integer) + ] + end + + let :foreign_keys do + [ + mock_foreign_key('fk_rails_cf2568e89e', 'foreign_thing_id', 'foreign_things'), + mock_foreign_key('custom_fk_name', 'other_thing_id', 'other_things'), + mock_foreign_key('fk_rails_a70234b26c', 'third_thing_id', 'third_things') + ] + end + + context 'when option "show_foreign_keys" is specified' do + let :options do + { show_foreign_keys: true } + end + + context 'when foreign_keys does not have option' do + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null, primary key + # foreign_thing_id :integer not null + # + # Foreign Keys + # + # custom_fk_name (other_thing_id => other_things.id) + # fk_rails_... (foreign_thing_id => foreign_things.id) + # fk_rails_... (third_thing_id => third_things.id) + # + EOS + end + + it 'returns schema info with foreign keys' do + is_expected.to eq(expected_result) + end + end + + context 'when foreign_keys have option "on_delete" and "on_update"' do + let :foreign_keys do + [ + mock_foreign_key('fk_rails_02e851e3b7', + 'foreign_thing_id', + 'foreign_things', + 'id', + on_delete: 'on_delete_value', + on_update: 'on_update_value') + ] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null, primary key + # foreign_thing_id :integer not null + # + # Foreign Keys + # + # fk_rails_... (foreign_thing_id => foreign_things.id) ON DELETE => on_delete_value ON UPDATE => on_update_value + # + EOS + end + + it 'returns schema info with foreign keys' do + is_expected.to eq(expected_result) + end + end + end + + context 'when option "show_foreign_keys" and "show_complete_foreign_keys" are specified' do + let :options do + { show_foreign_keys: true, show_complete_foreign_keys: true } + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null, primary key + # foreign_thing_id :integer not null + # + # Foreign Keys + # + # custom_fk_name (other_thing_id => other_things.id) + # fk_rails_a70234b26c (third_thing_id => third_things.id) + # fk_rails_cf2568e89e (foreign_thing_id => foreign_things.id) + # + EOS + end + + it 'returns schema info with foreign keys' do + is_expected.to eq(expected_result) + end + end + end + + context 'when "hide_limit_column_types" is specified in options' do + let :columns do + [ + mock_column(:id, :integer, limit: 8), + mock_column(:active, :boolean, limit: 1), + mock_column(:name, :string, limit: 50), + mock_column(:notes, :text, limit: 55) + ] + end + + context 'when "hide_limit_column_types" is blank string' do + let :options do + { hide_limit_column_types: '' } + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null, primary key + # active :boolean not null + # name :string(50) not null + # notes :text(55) not null + # + EOS + end + + it 'works with option "hide_limit_column_types"' do + is_expected.to eq expected_result + end + end + + context 'when "hide_limit_column_types" is "integer,boolean"' do + let :options do + { hide_limit_column_types: 'integer,boolean' } + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null, primary key + # active :boolean not null + # name :string(50) not null + # notes :text(55) not null + # + EOS + end + + it 'works with option "hide_limit_column_types"' do + is_expected.to eq expected_result + end + end + + context 'when "hide_limit_column_types" is "integer,boolean,string,text"' do + let :options do + { hide_limit_column_types: 'integer,boolean,string,text' } + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null, primary key + # active :boolean not null + # name :string not null + # notes :text not null + # + EOS + end + + it 'works with option "hide_limit_column_types"' do + is_expected.to eq expected_result + end + end + end + + context 'when "hide_default_column_types" is specified in options' do + let :columns do + [ + mock_column(:profile, :json, default: {}), + mock_column(:settings, :jsonb, default: {}), + mock_column(:parameters, :hstore, default: {}) + ] + end + + context 'when "hide_default_column_types" is blank string' do + let :options do + { hide_default_column_types: '' } + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # profile :json not null + # settings :jsonb not null + # parameters :hstore not null + # + EOS + end + + it 'works with option "hide_default_column_types"' do + is_expected.to eq expected_result + end + end + + context 'when "hide_default_column_types" is "skip"' do + let :options do + { hide_default_column_types: 'skip' } + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # profile :json default({}), not null + # settings :jsonb default({}), not null + # parameters :hstore default({}), not null + # + EOS + end + + it 'works with option "hide_default_column_types"' do + is_expected.to eq expected_result + end + end + + context 'when "hide_default_column_types" is "json"' do + let :options do + { hide_default_column_types: 'json' } + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # profile :json not null + # settings :jsonb default({}), not null + # parameters :hstore default({}), not null + # + EOS + end + + it 'works with option "hide_limit_column_types"' do + is_expected.to eq expected_result + end + end + end + + context 'when "classified_sort" is specified in options' do + let :columns do + [ + mock_column(:active, :boolean, limit: 1), + mock_column(:name, :string, limit: 50), + mock_column(:notes, :text, limit: 55) + ] + end + + context 'when "classified_sort" is "yes"' do + let :options do + { classified_sort: 'yes' } + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # active :boolean not null + # name :string(50) not null + # notes :text(55) not null + # + EOS + end + + it 'works with option "classified_sort"' do + is_expected.to eq expected_result + end + end + end + + context 'when "with_comment" is specified in options' do + context 'when "with_comment" is "yes"' do + let :options do + { with_comment: 'yes' } + end + + context 'when columns have comments' do + let :columns do + [ + mock_column(:id, :integer, limit: 8, comment: 'ID'), + mock_column(:active, :boolean, limit: 1, comment: 'Active'), + mock_column(:name, :string, limit: 50, comment: 'Name'), + mock_column(:notes, :text, limit: 55, comment: 'Notes'), + mock_column(:no_comment, :text, limit: 20, comment: nil) + ] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id(ID) :integer not null, primary key + # active(Active) :boolean not null + # name(Name) :string(50) not null + # notes(Notes) :text(55) not null + # no_comment :text(20) not null + # + EOS + end + + it 'works with option "with_comment"' do + is_expected.to eq expected_result + end + end + + context 'when columns have multibyte comments' do + let :columns do + [ + mock_column(:id, :integer, limit: 8, comment: 'ID'), + mock_column(:active, :boolean, limit: 1, comment: 'ACTIVE'), + mock_column(:name, :string, limit: 50, comment: 'NAME'), + mock_column(:notes, :text, limit: 55, comment: 'NOTES'), + mock_column(:cyrillic, :text, limit: 30, comment: 'Кириллица'), + mock_column(:japanese, :text, limit: 60, comment: '熊本大学 イタリア 宝島'), + mock_column(:arabic, :text, limit: 20, comment: 'لغة'), + mock_column(:no_comment, :text, limit: 20, comment: nil), + mock_column(:location, :geometry_collection, limit: nil, comment: nil) + ] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id(ID) :integer not null, primary key + # active(ACTIVE) :boolean not null + # name(NAME) :string(50) not null + # notes(NOTES) :text(55) not null + # cyrillic(Кириллица) :text(30) not null + # japanese(熊本大学 イタリア 宝島) :text(60) not null + # arabic(لغة) :text(20) not null + # no_comment :text(20) not null + # location :geometry_collect not null + # + EOS + end + + it 'works with option "with_comment"' do + is_expected.to eq expected_result + end + end + + context 'when columns have multiline comments' do + let :columns do + [ + mock_column(:id, :integer, limit: 8, comment: 'ID'), + mock_column(:notes, :text, limit: 55, comment: "Notes.\nMay include things like notes."), + mock_column(:no_comment, :text, limit: 20, comment: nil) + ] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id(ID) :integer not null, primary key + # notes(Notes.\\nMay include things like notes.):text(55) not null + # no_comment :text(20) not null + # + EOS + end + + it 'works with option "with_comment"' do + is_expected.to eq expected_result + end + end + + context 'when geometry columns are included' do + let :columns do + [ + mock_column(:id, :integer, limit: 8), + mock_column(:active, :boolean, default: false, null: false), + mock_column(:geometry, :geometry, + geometric_type: 'Geometry', srid: 4326, + limit: { srid: 4326, type: 'geometry' }), + mock_column(:location, :geography, + geometric_type: 'Point', srid: 0, + limit: { srid: 0, type: 'geometry' }) + ] + end + + let :expected_result do + <<~EOS + # Schema Info + # + # Table name: users + # + # id :integer not null, primary key + # active :boolean default(FALSE), not null + # geometry :geometry not null, geometry, 4326 + # location :geography not null, point, 0 + # + EOS + end + + it 'works with option "with_comment"' do + is_expected.to eq expected_result + end + end + end + end + end + end + end + + context 'when header is "== Schema Information"' do + let :header do + AnnotateModels::PREFIX + end + + context 'when the primary key is specified' do + context 'when the primary_key is :id' do + let :primary_key do + :id + end + + let :columns do + [ + mock_column(:id, :integer), + mock_column(:name, :string, limit: 50) + ] + end + + context 'when option "format_rdoc" is true' do + let :options do + { format_rdoc: true } + end + + let :expected_result do + <<~EOS + # == Schema Information + # + # Table name: users + # + # *id*:: integer, not null, primary key + # *name*:: string(50), not null + #-- + # == Schema Information End + #++ + EOS + end + + it 'returns schema info in RDoc format' do + is_expected.to eq(expected_result) + end + end + + context 'when option "format_yard" is true' do + let :options do + { format_yard: true } + end + + let :expected_result do + <<~EOS + # == Schema Information + # + # Table name: users + # + # @!attribute id + # @return [Integer] + # @!attribute name + # @return [String] + # + EOS + end + + it 'returns schema info in YARD format' do + is_expected.to eq(expected_result) + end + end + + context 'when option "format_markdown" is true' do + context 'when other option is not specified' do + let :options do + { format_markdown: true } + end + + let :expected_result do + <<~EOS + # == Schema Information + # + # Table name: `users` + # + # ### Columns + # + # Name | Type | Attributes + # ----------- | ------------------ | --------------------------- + # **`id`** | `integer` | `not null, primary key` + # **`name`** | `string(50)` | `not null` + # + EOS + end + + it 'returns schema info in Markdown format' do + is_expected.to eq(expected_result) + end + end + + context 'when option "show_indexes" is true' do + let :options do + { format_markdown: true, show_indexes: true } + end + + context 'when indexes are normal' do + let :indexes do + [ + mock_index('index_rails_02e851e3b7', columns: ['id']), + mock_index('index_rails_02e851e3b8', columns: ['foreign_thing_id']) + ] + end + + let :expected_result do + <<~EOS + # == Schema Information + # + # Table name: `users` + # + # ### Columns + # + # Name | Type | Attributes + # ----------- | ------------------ | --------------------------- + # **`id`** | `integer` | `not null, primary key` + # **`name`** | `string(50)` | `not null` + # + # ### Indexes + # + # * `index_rails_02e851e3b7`: + # * **`id`** + # * `index_rails_02e851e3b8`: + # * **`foreign_thing_id`** + # + EOS + end + + it 'returns schema info with index information in Markdown format' do + is_expected.to eq expected_result + end + end + + context 'when one of indexes includes "unique" clause' do + let :indexes do + [ + mock_index('index_rails_02e851e3b7', columns: ['id']), + mock_index('index_rails_02e851e3b8', + columns: ['foreign_thing_id'], + unique: true) + ] + end + + let :expected_result do + <<~EOS + # == Schema Information + # + # Table name: `users` + # + # ### Columns + # + # Name | Type | Attributes + # ----------- | ------------------ | --------------------------- + # **`id`** | `integer` | `not null, primary key` + # **`name`** | `string(50)` | `not null` + # + # ### Indexes + # + # * `index_rails_02e851e3b7`: + # * **`id`** + # * `index_rails_02e851e3b8` (_unique_): + # * **`foreign_thing_id`** + # + EOS + end + + it 'returns schema info with index information in Markdown format' do + is_expected.to eq expected_result + end + end + + context 'when one of indexes includes orderd index key' do + let :indexes do + [ + mock_index('index_rails_02e851e3b7', columns: ['id']), + mock_index('index_rails_02e851e3b8', + columns: ['foreign_thing_id'], + orders: { 'foreign_thing_id' => :desc }) + ] + end + + let :expected_result do + <<~EOS + # == Schema Information + # + # Table name: `users` + # + # ### Columns + # + # Name | Type | Attributes + # ----------- | ------------------ | --------------------------- + # **`id`** | `integer` | `not null, primary key` + # **`name`** | `string(50)` | `not null` + # + # ### Indexes + # + # * `index_rails_02e851e3b7`: + # * **`id`** + # * `index_rails_02e851e3b8`: + # * **`foreign_thing_id DESC`** + # + EOS + end + + it 'returns schema info with index information in Markdown format' do + is_expected.to eq expected_result + end + end + + context 'when one of indexes includes "where" clause and "unique" clause' do + let :indexes do + [ + mock_index('index_rails_02e851e3b7', columns: ['id']), + mock_index('index_rails_02e851e3b8', + columns: ['foreign_thing_id'], + unique: true, + where: 'name IS NOT NULL') + ] + end + + let :expected_result do + <<~EOS + # == Schema Information + # + # Table name: `users` + # + # ### Columns + # + # Name | Type | Attributes + # ----------- | ------------------ | --------------------------- + # **`id`** | `integer` | `not null, primary key` + # **`name`** | `string(50)` | `not null` + # + # ### Indexes + # + # * `index_rails_02e851e3b7`: + # * **`id`** + # * `index_rails_02e851e3b8` (_unique_ _where_ name IS NOT NULL): + # * **`foreign_thing_id`** + # + EOS + end + + it 'returns schema info with index information in Markdown format' do + is_expected.to eq expected_result + end + end + + context 'when one of indexes includes "using" clause other than "btree"' do + let :indexes do + [ + mock_index('index_rails_02e851e3b7', columns: ['id']), + mock_index('index_rails_02e851e3b8', + columns: ['foreign_thing_id'], + using: 'hash') + ] + end + + let :expected_result do + <<~EOS + # == Schema Information + # + # Table name: `users` + # + # ### Columns + # + # Name | Type | Attributes + # ----------- | ------------------ | --------------------------- + # **`id`** | `integer` | `not null, primary key` + # **`name`** | `string(50)` | `not null` + # + # ### Indexes + # + # * `index_rails_02e851e3b7`: + # * **`id`** + # * `index_rails_02e851e3b8` (_using_ hash): + # * **`foreign_thing_id`** + # + EOS + end + + it 'returns schema info with index information in Markdown format' do + is_expected.to eq expected_result + end + end + end + + context 'when option "show_foreign_keys" is true' do + let :options do + { format_markdown: true, show_foreign_keys: true } + end + + let :columns do + [ + mock_column(:id, :integer), + mock_column(:foreign_thing_id, :integer) + ] + end + + context 'when foreign_keys have option "on_delete" and "on_update"' do + let :foreign_keys do + [ + mock_foreign_key('fk_rails_02e851e3b7', + 'foreign_thing_id', + 'foreign_things', + 'id', + on_delete: 'on_delete_value', + on_update: 'on_update_value') + ] + end + + let :expected_result do + <<~EOS + # == Schema Information + # + # Table name: `users` + # + # ### Columns + # + # Name | Type | Attributes + # ----------------------- | ------------------ | --------------------------- + # **`id`** | `integer` | `not null, primary key` + # **`foreign_thing_id`** | `integer` | `not null` + # + # ### Foreign Keys + # + # * `fk_rails_...` (_ON DELETE => on_delete_value ON UPDATE => on_update_value_): + # * **`foreign_thing_id => foreign_things.id`** + # + EOS + end + + it 'returns schema info with foreign_keys in Markdown format' do + is_expected.to eq(expected_result) + end + end + end + end + + context 'when "format_doc" and "with_comment" are specified in options' do + let :options do + { format_rdoc: true, with_comment: true } + end + + context 'when columns are normal' do + let :columns do + [ + mock_column(:id, :integer, comment: 'ID'), + mock_column(:name, :string, limit: 50, comment: 'Name') + ] + end + + let :expected_result do + <<~EOS + # == Schema Information + # + # Table name: users + # + # *id(ID)*:: integer, not null, primary key + # *name(Name)*:: string(50), not null + #-- + # == Schema Information End + #++ + EOS + end + + it 'returns schema info in RDoc format' do + is_expected.to eq expected_result + end + end + end + + context 'when "format_markdown" and "with_comment" are specified in options' do + let :options do + { format_markdown: true, with_comment: true } + end + + context 'when columns have comments' do + let :columns do + [ + mock_column(:id, :integer, comment: 'ID'), + mock_column(:name, :string, limit: 50, comment: 'Name') + ] + end + + let :expected_result do + <<~EOS + # == Schema Information + # + # Table name: `users` + # + # ### Columns + # + # Name | Type | Attributes + # ----------------- | ------------------ | --------------------------- + # **`id(ID)`** | `integer` | `not null, primary key` + # **`name(Name)`** | `string(50)` | `not null` + # + EOS + end + + it 'returns schema info in Markdown format' do + is_expected.to eq expected_result + end + end + + context 'when columns have multibyte comments' do + let :columns do + [ + mock_column(:id, :integer, comment: 'ID'), + mock_column(:name, :string, limit: 50, comment: 'NAME') + ] + end + + let :expected_result do + <<~EOS + # == Schema Information + # + # Table name: `users` + # + # ### Columns + # + # Name | Type | Attributes + # --------------------- | ------------------ | --------------------------- + # **`id(ID)`** | `integer` | `not null, primary key` + # **`name(NAME)`** | `string(50)` | `not null` + # + EOS + end + + it 'returns schema info in Markdown format' do + is_expected.to eq expected_result + end + end + end + end + end + end + end + end +end diff --git a/spec/lib/annotate/models/model_spec_helper.rb b/spec/lib/annotate/models/model_spec_helper.rb new file mode 100644 index 000000000..00f581e6e --- /dev/null +++ b/spec/lib/annotate/models/model_spec_helper.rb @@ -0,0 +1,57 @@ +def mock_index(name, params = {}) + double('IndexKeyDefinition', + name: name, + columns: params[:columns] || [], + unique: params[:unique] || false, + orders: params[:orders] || {}, + where: params[:where], + using: params[:using]) +end + +def mock_foreign_key(name, from_column, to_table, to_column = 'id', constraints = {}) + double('ForeignKeyDefinition', + name: name, + column: from_column, + to_table: to_table, + primary_key: to_column, + on_delete: constraints[:on_delete], + on_update: constraints[:on_update]) +end + +def mock_connection(indexes = [], foreign_keys = []) + double('Conn', + indexes: indexes, + foreign_keys: foreign_keys, + supports_foreign_keys?: true) +end + +def mock_class(table_name, primary_key, columns, indexes = [], foreign_keys = []) + options = { + connection: mock_connection(indexes, foreign_keys), + table_exists?: true, + table_name: table_name, + primary_key: primary_key, + column_names: columns.map { |col| col.name.to_s }, + columns: columns, + column_defaults: Hash[columns.map { |col| [col.name, col.default] }], + table_name_prefix: '' + } + + double('An ActiveRecord class', options) +end + +def mock_column(name, type, options = {}) + default_options = { + limit: nil, + null: false, + default: nil, + sql_type: type + } + + stubs = default_options.dup + stubs.merge!(options) + stubs[:name] = name + stubs[:type] = type + + double('Column', stubs) +end diff --git a/spec/lib/annotate/models/parse_options_spec.rb b/spec/lib/annotate/models/parse_options_spec.rb new file mode 100644 index 000000000..bb1192bc5 --- /dev/null +++ b/spec/lib/annotate/models/parse_options_spec.rb @@ -0,0 +1,76 @@ +require_relative '../../../spec_helper' +require 'annotate/annotate_models' +require 'annotate/active_record_patch' +require 'active_support/core_ext/string' +require 'files' +require 'tmpdir' + +describe AnnotateModels do + describe '.parse_options' do + let(:options) do + { + root_dir: '/root', + model_dir: 'app/models,app/one, app/two ,,app/three', + skip_subdirectory_model_load: false + } + end + + before :each do + AnnotateModels.send(:parse_options, options) + end + + describe '@root_dir' do + subject do + AnnotateModels.instance_variable_get(:@root_dir) + end + + it 'sets @root_dir' do + is_expected.to eq('/root') + end + end + + describe '@model_dir' do + subject do + AnnotateModels.instance_variable_get(:@model_dir) + end + + it 'separates option "model_dir" with commas and sets @model_dir as an array of string' do + is_expected.to eq(['app/models', 'app/one', 'app/two', 'app/three']) + end + end + + describe '@skip_subdirectory_model_load' do + subject do + AnnotateModels.instance_variable_get(:@skip_subdirectory_model_load) + end + + context 'option is set to true' do + let(:options) do + { + root_dir: '/root', + model_dir: 'app/models,app/one, app/two ,,app/three', + skip_subdirectory_model_load: true + } + end + + it 'sets skip_subdirectory_model_load to true' do + is_expected.to eq(true) + end + end + + context 'option is set to false' do + let(:options) do + { + root_dir: '/root', + model_dir: 'app/models,app/one, app/two ,,app/three', + skip_subdirectory_model_load: false + } + end + + it 'sets skip_subdirectory_model_load to false' do + is_expected.to eq(false) + end + end + end + end +end diff --git a/spec/lib/annotate/models/quote_spec.rb b/spec/lib/annotate/models/quote_spec.rb new file mode 100644 index 000000000..1a30c05e7 --- /dev/null +++ b/spec/lib/annotate/models/quote_spec.rb @@ -0,0 +1,72 @@ +require_relative '../../../spec_helper' +require 'annotate/annotate_models' +require 'annotate/active_record_patch' +require 'active_support/core_ext/string' +require 'files' +require 'tmpdir' + +describe AnnotateModels do + describe '.quote' do + subject do + AnnotateModels.quote(value) + end + + context 'when the argument is nil' do + let(:value) { nil } + it 'returns string "NULL"' do + is_expected.to eq('NULL') + end + end + + context 'when the argument is true' do + let(:value) { true } + it 'returns string "TRUE"' do + is_expected.to eq('TRUE') + end + end + + context 'when the argument is false' do + let(:value) { false } + it 'returns string "FALSE"' do + is_expected.to eq('FALSE') + end + end + + context 'when the argument is an integer' do + let(:value) { 25 } + it 'returns the integer as a string' do + is_expected.to eq('25') + end + end + + context 'when the argument is a float number' do + context 'when the argument is like 25.6' do + let(:value) { 25.6 } + it 'returns the float number as a string' do + is_expected.to eq('25.6') + end + end + + context 'when the argument is like 1e-20' do + let(:value) { 1e-20 } + it 'returns the float number as a string' do + is_expected.to eq('1.0e-20') + end + end + end + + context 'when the argument is a BigDecimal number' do + let(:value) { BigDecimal('1.2') } + it 'returns the float number as a string' do + is_expected.to eq('1.2') + end + end + + context 'when the argument is an array' do + let(:value) { [BigDecimal('1.2')] } + it 'returns an array of which elements are converted to string' do + is_expected.to eq(['1.2']) + end + end + end +end diff --git a/spec/lib/annotate/models/remove_annotation_of_file_spec.rb b/spec/lib/annotate/models/remove_annotation_of_file_spec.rb new file mode 100644 index 000000000..d2f9ea4fc --- /dev/null +++ b/spec/lib/annotate/models/remove_annotation_of_file_spec.rb @@ -0,0 +1,241 @@ +require_relative '../../../spec_helper' +require_relative 'model_spec_helper' +require 'annotate/annotate_models' +require 'annotate/active_record_patch' +require 'active_support/core_ext/string' +require 'files' +require 'tmpdir' + +describe AnnotateModels do + describe '.remove_annotation_of_file' do + subject do + AnnotateModels.remove_annotation_of_file(path) + end + + let :tmpdir do + Dir.mktmpdir('annotate_models') + end + + let :path do + File.join(tmpdir, filename).tap do |path| + File.open(path, 'w') do |f| + f.puts(file_content) + end + end + end + + let :file_content_after_removal do + subject + File.read(path) + end + + let :expected_result do + <<~EOS + class Foo < ActiveRecord::Base + end + EOS + end + + context 'when annotation is before main content' do + let :filename do + 'before.rb' + end + + let :file_content do + <<~EOS + # == Schema Information + # + # Table name: foo + # + # id :integer not null, primary key + # created_at :datetime + # updated_at :datetime + # + + class Foo < ActiveRecord::Base + end + EOS + end + + it 'removes annotation' do + expect(file_content_after_removal).to eq expected_result + end + end + + context 'when annotation is before main content and CRLF is used for line breaks' do + let :filename do + 'before.rb' + end + + let :file_content do + <<~EOS + # == Schema Information + # + # Table name: foo\r\n# + # id :integer not null, primary key + # created_at :datetime + # updated_at :datetime + # + \r\n + class Foo < ActiveRecord::Base + end + EOS + end + + it 'removes annotation' do + expect(file_content_after_removal).to eq expected_result + end + end + + context 'when annotation is before main content and with opening wrapper' do + let :filename do + 'opening_wrapper.rb' + end + + let :file_content do + <<~EOS + # wrapper + # == Schema Information + # + # Table name: foo + # + # id :integer not null, primary key + # created_at :datetime + # updated_at :datetime + # + + class Foo < ActiveRecord::Base + end + EOS + end + + subject do + AnnotateModels.remove_annotation_of_file(path, wrapper_open: 'wrapper') + end + + it 'removes annotation' do + expect(file_content_after_removal).to eq expected_result + end + end + + context 'when annotation is before main content and with opening wrapper' do + let :filename do + 'opening_wrapper.rb' + end + + let :file_content do + <<~EOS + # wrapper\r\n# == Schema Information + # + # Table name: foo + # + # id :integer not null, primary key + # created_at :datetime + # updated_at :datetime + # + + class Foo < ActiveRecord::Base + end + EOS + end + + subject do + AnnotateModels.remove_annotation_of_file(path, wrapper_open: 'wrapper') + end + + it 'removes annotation' do + expect(file_content_after_removal).to eq expected_result + end + end + + context 'when annotation is after main content' do + let :filename do + 'after.rb' + end + + let :file_content do + <<~EOS + class Foo < ActiveRecord::Base + end + + # == Schema Information + # + # Table name: foo + # + # id :integer not null, primary key + # created_at :datetime + # updated_at :datetime + # + + EOS + end + + it 'removes annotation' do + expect(file_content_after_removal).to eq expected_result + end + end + + context 'when annotation is after main content and with closing wrapper' do + let :filename do + 'closing_wrapper.rb' + end + + let :file_content do + <<~EOS + class Foo < ActiveRecord::Base + end + + # == Schema Information + # + # Table name: foo + # + # id :integer not null, primary key + # created_at :datetime + # updated_at :datetime + # + # wrapper + + EOS + end + + subject do + AnnotateModels.remove_annotation_of_file(path, wrapper_close: 'wrapper') + end + + it 'removes annotation' do + expect(file_content_after_removal).to eq expected_result + end + end + + context 'when annotation is before main content and with comment "-*- SkipSchemaAnnotations"' do + let :filename do + 'skip.rb' + end + + let :file_content do + <<~EOS + # -*- SkipSchemaAnnotations + # == Schema Information + # + # Table name: foo + # + # id :integer not null, primary key + # created_at :datetime + # updated_at :datetime + # + + class Foo < ActiveRecord::Base + end + EOS + end + + let :expected_result do + file_content + end + + it 'does not remove annotation' do + expect(file_content_after_removal).to eq expected_result + end + end + end +end diff --git a/spec/lib/annotate/models/resolve_filename_spec.rb b/spec/lib/annotate/models/resolve_filename_spec.rb new file mode 100644 index 000000000..0f42e258b --- /dev/null +++ b/spec/lib/annotate/models/resolve_filename_spec.rb @@ -0,0 +1,65 @@ +require_relative '../../../spec_helper' +require_relative 'model_spec_helper' +require 'annotate/annotate_models' +require 'annotate/active_record_patch' +require 'active_support/core_ext/string' +require 'files' +require 'tmpdir' + +describe AnnotateModels do + describe '.resolve_filename' do + subject do + AnnotateModels.resolve_filename(filename_template, model_name, table_name) + end + + context 'When model_name is "example_model" and table_name is "example_models"' do + let(:model_name) { 'example_model' } + let(:table_name) { 'example_models' } + + context "when filename_template is 'test/unit/%MODEL_NAME%_test.rb'" do + let(:filename_template) { 'test/unit/%MODEL_NAME%_test.rb' } + + it 'returns the test path for a model' do + is_expected.to eq 'test/unit/example_model_test.rb' + end + end + + context "when filename_template is '/foo/bar/%MODEL_NAME%/testing.rb'" do + let(:filename_template) { '/foo/bar/%MODEL_NAME%/testing.rb' } + + it 'returns the additional glob' do + is_expected.to eq '/foo/bar/example_model/testing.rb' + end + end + + context "when filename_template is '/foo/bar/%PLURALIZED_MODEL_NAME%/testing.rb'" do + let(:filename_template) { '/foo/bar/%PLURALIZED_MODEL_NAME%/testing.rb' } + + it 'returns the additional glob' do + is_expected.to eq '/foo/bar/example_models/testing.rb' + end + end + + context "when filename_template is 'test/fixtures/%TABLE_NAME%.yml'" do + let(:filename_template) { 'test/fixtures/%TABLE_NAME%.yml' } + + it 'returns the fixture path for a model' do + is_expected.to eq 'test/fixtures/example_models.yml' + end + end + end + + context 'When model_name is "parent/child" and table_name is "parent_children"' do + let(:model_name) { 'parent/child' } + let(:table_name) { 'parent_children' } + + context "when filename_template is 'test/fixtures/%PLURALIZED_MODEL_NAME%.yml'" do + let(:filename_template) { 'test/fixtures/%PLURALIZED_MODEL_NAME%.yml' } + + it 'returns the fixture path for a nested model' do + is_expected.to eq 'test/fixtures/parent/children.yml' + end + end + end + end +end diff --git a/spec/lib/annotate/models/set_defaults_spec.rb b/spec/lib/annotate/models/set_defaults_spec.rb new file mode 100644 index 000000000..91fe285f9 --- /dev/null +++ b/spec/lib/annotate/models/set_defaults_spec.rb @@ -0,0 +1,34 @@ +require_relative '../../../spec_helper' +require 'annotate/annotate_models' +require 'annotate/active_record_patch' +require 'active_support/core_ext/string' +require 'files' +require 'tmpdir' + +describe AnnotateModels do + describe '.set_defaults' do + subject do + Annotate::Helpers.true?(ENV['show_complete_foreign_keys']) + end + + context 'when default value of "show_complete_foreign_keys" is not set' do + it 'returns false' do + is_expected.to be(false) + end + end + + context 'when default value of "show_complete_foreign_keys" is set' do + before do + Annotate.set_defaults('show_complete_foreign_keys' => 'true') + end + + it 'returns true' do + is_expected.to be(true) + end + end + + after :each do + ENV.delete('show_complete_foreign_keys') + end + end +end diff --git a/spec/lib/annotate/annotate_routes_spec.rb b/spec/lib/annotate/routes/routes_spec.rb similarity index 97% rename from spec/lib/annotate/annotate_routes_spec.rb rename to spec/lib/annotate/routes/routes_spec.rb index a0ed118cc..6a7232f4e 100644 --- a/spec/lib/annotate/annotate_routes_spec.rb +++ b/spec/lib/annotate/routes/routes_spec.rb @@ -1,6 +1,20 @@ -require_relative '../../spec_helper' +require_relative '../../../spec_helper' require 'annotate/annotate_routes' +MAGIC_COMMENTS = [ + '# encoding: UTF-8', + '# coding: UTF-8', + '# -*- coding: UTF-8 -*-', + '#encoding: utf-8', + '# encoding: utf-8', + '# -*- encoding : utf-8 -*-', + "# encoding: utf-8\n# frozen_string_literal: true", + "# frozen_string_literal: true\n# encoding: utf-8", + '# frozen_string_literal: true', + '#frozen_string_literal: false', + '# -*- frozen_string_literal : true -*-' +].freeze + describe AnnotateRoutes do ROUTE_FILE = 'config/routes.rb'.freeze @@ -9,20 +23,6 @@ MESSAGE_NOT_FOUND = "#{ROUTE_FILE} could not be found.".freeze MESSAGE_REMOVED = "Annotations were removed from #{ROUTE_FILE}.".freeze - MAGIC_COMMENTS = [ - '# encoding: UTF-8', - '# coding: UTF-8', - '# -*- coding: UTF-8 -*-', - '#encoding: utf-8', - '# encoding: utf-8', - '# -*- encoding : utf-8 -*-', - "# encoding: utf-8\n# frozen_string_literal: true", - "# frozen_string_literal: true\n# encoding: utf-8", - '# frozen_string_literal: true', - '#frozen_string_literal: false', - '# -*- frozen_string_literal : true -*-' - ].freeze - let :stubs do {} end @@ -97,7 +97,7 @@ # ## Route Map # - # Prefix | Verb | URI Pattern | Controller#Action + # Prefix | Verb | URI Pattern | Controller#Action#{' '} # --------- | ---------- | --------------- | -------------------- # myaction1 | GET | /url1(.:format) | mycontroller1#action # myaction2 | POST | /url2(.:format) | mycontroller2#action @@ -189,7 +189,7 @@ # ## Route Map # - # Prefix | Verb | URI Pattern | Controller#Action + # Prefix | Verb | URI Pattern | Controller#Action#{' '} # --------- | ---------- | --------------- | -------------------- # myaction1 | GET | /url1(.:format) | mycontroller1#action # myaction2 | POST | /url2(.:format) | mycontroller2#action