shoulda-matchers 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +3 -3
  4. data/CONTRIBUTING.md +60 -28
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +15 -12
  7. data/NEWS.md +111 -0
  8. data/README.md +94 -6
  9. data/Rakefile +10 -8
  10. data/custom_plan.rb +88 -0
  11. data/gemfiles/4.0.0.gemfile +1 -0
  12. data/gemfiles/4.0.0.gemfile.lock +21 -18
  13. data/gemfiles/4.0.1.gemfile +1 -0
  14. data/gemfiles/4.0.1.gemfile.lock +21 -18
  15. data/gemfiles/4.1.gemfile +1 -0
  16. data/gemfiles/4.1.gemfile.lock +21 -18
  17. data/gemfiles/4.2.gemfile +1 -0
  18. data/gemfiles/4.2.gemfile.lock +24 -21
  19. data/lib/shoulda/matchers/action_controller/permit_matcher.rb +6 -11
  20. data/lib/shoulda/matchers/active_model.rb +10 -1
  21. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +258 -180
  22. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_changed_value_error.rb +45 -0
  23. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_does_not_exist_error.rb +23 -0
  24. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setter.rb +236 -0
  25. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setter_and_validator.rb +62 -0
  26. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters.rb +40 -0
  27. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters_and_validators.rb +48 -0
  28. data/lib/shoulda/matchers/active_model/allow_value_matcher/successful_check.rb +14 -0
  29. data/lib/shoulda/matchers/active_model/allow_value_matcher/successful_setting.rb +14 -0
  30. data/lib/shoulda/matchers/active_model/disallow_value_matcher.rb +34 -14
  31. data/lib/shoulda/matchers/active_model/helpers.rb +9 -17
  32. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +13 -6
  33. data/lib/shoulda/matchers/active_model/numericality_matchers/even_number_matcher.rb +13 -2
  34. data/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +19 -35
  35. data/lib/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher.rb +13 -2
  36. data/lib/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher.rb +12 -2
  37. data/lib/shoulda/matchers/active_model/qualifiers.rb +12 -0
  38. data/lib/shoulda/matchers/active_model/qualifiers/ignore_interference_by_writer.rb +101 -0
  39. data/lib/shoulda/matchers/active_model/qualifiers/ignoring_interference_by_writer.rb +21 -0
  40. data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +30 -32
  41. data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +5 -8
  42. data/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +22 -22
  43. data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +27 -16
  44. data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +58 -15
  45. data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +22 -12
  46. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +165 -87
  47. data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +7 -9
  48. data/lib/shoulda/matchers/active_model/validation_matcher.rb +111 -49
  49. data/lib/shoulda/matchers/active_model/validation_matcher/build_description.rb +60 -0
  50. data/lib/shoulda/matchers/active_model/validator.rb +71 -52
  51. data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +19 -5
  52. data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +450 -124
  53. data/lib/shoulda/matchers/util.rb +43 -0
  54. data/lib/shoulda/matchers/util/word_wrap.rb +59 -31
  55. data/lib/shoulda/matchers/version.rb +1 -1
  56. data/script/update_gem_in_all_appraisals +1 -1
  57. data/script/update_gems_in_all_appraisals +1 -1
  58. data/spec/acceptance/multiple_libraries_integration_spec.rb +5 -2
  59. data/spec/acceptance/rails_integration_spec.rb +6 -2
  60. data/spec/spec_helper.rb +1 -3
  61. data/spec/support/acceptance/helpers/step_helpers.rb +4 -1
  62. data/spec/support/tests/current_bundle.rb +21 -7
  63. data/spec/support/unit/active_record/create_table.rb +54 -0
  64. data/spec/support/unit/attribute.rb +47 -0
  65. data/spec/support/unit/capture.rb +6 -0
  66. data/spec/support/unit/change_value.rb +111 -0
  67. data/spec/support/unit/create_model_arguments/basic.rb +135 -0
  68. data/spec/support/unit/create_model_arguments/has_many.rb +15 -0
  69. data/spec/support/unit/create_model_arguments/uniqueness_matcher.rb +74 -0
  70. data/spec/support/unit/helpers/active_record_versions.rb +1 -1
  71. data/spec/support/unit/helpers/class_builder.rb +61 -47
  72. data/spec/support/unit/helpers/database_helpers.rb +5 -3
  73. data/spec/support/unit/helpers/model_builder.rb +77 -97
  74. data/spec/support/unit/helpers/validation_matcher_scenario_helpers.rb +44 -0
  75. data/spec/support/unit/load_environment.rb +12 -0
  76. data/spec/support/unit/matchers/fail_with_message_including_matcher.rb +2 -2
  77. data/spec/support/unit/matchers/fail_with_message_matcher.rb +12 -1
  78. data/spec/support/unit/model_creation_strategies/active_model.rb +111 -0
  79. data/spec/support/unit/model_creation_strategies/active_record.rb +77 -0
  80. data/spec/support/unit/model_creators.rb +19 -0
  81. data/spec/support/unit/model_creators/active_model.rb +39 -0
  82. data/spec/support/unit/model_creators/active_record.rb +43 -0
  83. data/spec/support/unit/model_creators/active_record/has_and_belongs_to_many.rb +95 -0
  84. data/spec/support/unit/model_creators/active_record/has_many.rb +67 -0
  85. data/spec/support/unit/model_creators/active_record/uniqueness_matcher.rb +42 -0
  86. data/spec/support/unit/model_creators/basic.rb +97 -0
  87. data/spec/support/unit/rails_application.rb +1 -1
  88. data/spec/support/unit/record_validating_confirmation_builder.rb +3 -7
  89. data/spec/support/unit/shared_examples/ignoring_interference_by_writer.rb +79 -0
  90. data/spec/support/unit/validation_matcher_scenario.rb +62 -0
  91. data/spec/unit/shoulda/matchers/active_model/allow_mass_assignment_of_matcher_spec.rb +4 -0
  92. data/spec/unit/shoulda/matchers/active_model/allow_value_matcher_spec.rb +575 -140
  93. data/spec/unit/shoulda/matchers/active_model/validate_absence_of_matcher_spec.rb +115 -15
  94. data/spec/unit/shoulda/matchers/active_model/validate_acceptance_of_matcher_spec.rb +42 -4
  95. data/spec/unit/shoulda/matchers/active_model/validate_confirmation_of_matcher_spec.rb +92 -6
  96. data/spec/unit/shoulda/matchers/active_model/validate_exclusion_of_matcher_spec.rb +122 -10
  97. data/spec/unit/shoulda/matchers/active_model/validate_inclusion_of_matcher_spec.rb +306 -58
  98. data/spec/unit/shoulda/matchers/active_model/validate_length_of_matcher_spec.rb +122 -3
  99. data/spec/unit/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +805 -131
  100. data/spec/unit/shoulda/matchers/active_model/validate_presence_of_matcher_spec.rb +196 -29
  101. data/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb +82 -40
  102. data/spec/unit/shoulda/matchers/active_record/validate_uniqueness_of_matcher_spec.rb +600 -101
  103. data/spec/unit/shoulda/matchers/util/word_wrap_spec.rb +88 -33
  104. data/spec/unit_spec_helper.rb +10 -22
  105. data/zeus.json +11 -0
  106. metadata +64 -23
  107. data/lib/shoulda/matchers/active_model/strict_validator.rb +0 -51
  108. data/spec/support/unit/shared_examples/numerical_type_submatcher.rb +0 -15
  109. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/comparison_matcher_spec.rb +0 -288
  110. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/even_number_matcher_spec.rb +0 -100
  111. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher_spec.rb +0 -100
  112. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher_spec.rb +0 -100
@@ -30,6 +30,49 @@ module Shoulda
30
30
  indentation = ' ' * width
31
31
  string.split(/[\n\r]/).map { |line| indentation + line }.join("\n")
32
32
  end
33
+
34
+ def self.a_or_an(next_word)
35
+ if next_word =~ /\A[aeiou]/i
36
+ "an #{next_word}"
37
+ else
38
+ "a #{next_word}"
39
+ end
40
+ end
41
+
42
+ def self.inspect_value(value)
43
+ "‹#{value.inspect}›"
44
+ end
45
+
46
+ def self.inspect_values(values)
47
+ values.map { |value| inspect_value(value) }
48
+ end
49
+
50
+ def self.inspect_range(range)
51
+ "#{inspect_value(range.first)} to #{inspect_value(range.last)}"
52
+ end
53
+
54
+ def self.dummy_value_for(column_type, array: false)
55
+ if array
56
+ [dummy_value_for(column_type, array: false)]
57
+ else
58
+ case column_type
59
+ when :integer
60
+ 0
61
+ when :date
62
+ Date.new(2100, 1, 1)
63
+ when :datetime, :timestamp
64
+ DateTime.new(2100, 1, 1)
65
+ when :time
66
+ Time.new(2100, 1, 1)
67
+ when :uuid
68
+ SecureRandom.uuid
69
+ when :boolean
70
+ true
71
+ else
72
+ 'dummy value'
73
+ end
74
+ end
75
+ end
33
76
  end
34
77
  end
35
78
  end
@@ -1,14 +1,15 @@
1
1
  module Shoulda
2
2
  module Matchers
3
3
  # @private
4
- def self.word_wrap(document)
5
- Document.new(document).wrap
4
+ def self.word_wrap(document, options = {})
5
+ Document.new(document, options).wrap
6
6
  end
7
7
 
8
8
  # @private
9
9
  class Document
10
- def initialize(document)
10
+ def initialize(document, indent: 0)
11
11
  @document = document
12
+ @indent = indent
12
13
  end
13
14
 
14
15
  def wrap
@@ -17,7 +18,7 @@ module Shoulda
17
18
 
18
19
  protected
19
20
 
20
- attr_reader :document
21
+ attr_reader :document, :indent
21
22
 
22
23
  private
23
24
 
@@ -27,7 +28,7 @@ module Shoulda
27
28
 
28
29
  def wrapped_paragraphs
29
30
  paragraphs.map do |paragraph|
30
- Paragraph.new(paragraph).wrap
31
+ Paragraph.new(paragraph, indent: indent).wrap
31
32
  end
32
33
  end
33
34
  end
@@ -51,8 +52,9 @@ module Shoulda
51
52
 
52
53
  # @private
53
54
  class Paragraph
54
- def initialize(paragraph)
55
+ def initialize(paragraph, indent: 0)
55
56
  @paragraph = Text.new(paragraph)
57
+ @indent = indent
56
58
  end
57
59
 
58
60
  def wrap
@@ -67,7 +69,7 @@ module Shoulda
67
69
 
68
70
  protected
69
71
 
70
- attr_reader :paragraph
72
+ attr_reader :paragraph, :indent
71
73
 
72
74
  private
73
75
 
@@ -92,11 +94,11 @@ module Shoulda
92
94
  end
93
95
 
94
96
  def wrap_lines(lines)
95
- lines.map { |line| Line.new(line).wrap }
97
+ lines.map { |line| Line.new(line, indent: indent).wrap }
96
98
  end
97
99
 
98
100
  def wrap_generic_paragraph
99
- Line.new(combine_paragraph_into_one_line).wrap
101
+ Line.new(combine_paragraph_into_one_line, indent: indent).wrap
100
102
  end
101
103
 
102
104
  def combine_paragraph_into_one_line
@@ -107,72 +109,98 @@ module Shoulda
107
109
  # @private
108
110
  class Line
109
111
  TERMINAL_WIDTH = 72
112
+ OFFSETS = { left: -1, right: +1 }
110
113
 
111
- def initialize(line)
114
+ def initialize(line, indent: 0)
115
+ @indent = indent
112
116
  @original_line = @line_to_wrap = Text.new(line)
113
- @indentation = nil
117
+ @indentation = ' ' * indent
118
+ @indentation_read = false
114
119
  end
115
120
 
116
121
  def wrap
117
- lines = []
118
-
119
122
  if line_to_wrap.indented?
120
- lines << line_to_wrap
123
+ [line_to_wrap]
121
124
  else
125
+ lines = []
126
+
122
127
  loop do
128
+ @previous_line_to_wrap = line_to_wrap
123
129
  new_line = (indentation || '') + line_to_wrap
124
130
  result = wrap_line(new_line)
125
- lines << result[:fitted_line].rstrip
126
- @indentation ||= read_indentation
131
+ lines << normalize_whitespace(result[:fitted_line])
132
+
133
+ unless @indentation_read
134
+ @indentation = read_indentation
135
+ @indentation_read = true
136
+ end
137
+
127
138
  @line_to_wrap = result[:leftover]
128
139
 
129
- if line_to_wrap.empty? || @original_line == @line_to_wrap
140
+ if line_to_wrap.to_s.empty? || previous_line_to_wrap == line_to_wrap
130
141
  break
131
142
  end
132
143
  end
133
- end
134
144
 
135
- lines
145
+ lines
146
+ end
136
147
  end
137
148
 
138
149
  protected
139
150
 
140
- attr_reader :original_line, :line_to_wrap, :indentation
151
+ attr_reader :indent, :original_line, :line_to_wrap, :indentation,
152
+ :previous_line_to_wrap
141
153
 
142
154
  private
143
155
 
144
156
  def read_indentation
157
+ initial_indentation = ' ' * indent
145
158
  match = line_to_wrap.match_as_list_item
146
159
 
147
160
  if match
148
- ' ' * match[1].length
161
+ initial_indentation + (' ' * match[1].length)
149
162
  else
150
- ''
163
+ initial_indentation
151
164
  end
152
165
  end
153
166
 
154
- def wrap_line(line)
167
+ def wrap_line(line, direction: :left)
168
+ index = nil
169
+
155
170
  if line.length > TERMINAL_WIDTH
156
- index = determine_where_to_break_line(line)
157
- fitted_line = line[0 .. index].rstrip
158
- leftover = line[index + 1 .. -1]
159
- else
171
+ index = determine_where_to_break_line(line, direction: :left)
172
+
173
+ if index == -1
174
+ index = determine_where_to_break_line(line, direction: :right)
175
+ end
176
+ end
177
+
178
+ if index.nil? || index == -1
160
179
  fitted_line = line
161
180
  leftover = ''
181
+ else
182
+ fitted_line = line[0..index].rstrip
183
+ leftover = line[index + 1 .. -1]
162
184
  end
163
185
 
164
186
  { fitted_line: fitted_line, leftover: leftover }
165
187
  end
166
188
 
167
- def determine_where_to_break_line(line)
168
- index = TERMINAL_WIDTH - 1
189
+ def determine_where_to_break_line(line, args)
190
+ direction = args.fetch(:direction)
191
+ index = TERMINAL_WIDTH
192
+ offset = OFFSETS.fetch(direction)
169
193
 
170
- while line[index] !~ /\s/
171
- index -= 1
194
+ while line[index] !~ /\s/ && (0...line.length).cover?(index)
195
+ index += offset
172
196
  end
173
197
 
174
198
  index
175
199
  end
200
+
201
+ def normalize_whitespace(string)
202
+ indentation + string.strip.squeeze(' ')
203
+ end
176
204
  end
177
205
  end
178
206
  end
@@ -1,6 +1,6 @@
1
1
  module Shoulda
2
2
  module Matchers
3
3
  # @private
4
- VERSION = '3.0.1'.freeze
4
+ VERSION = '3.1.0'.freeze
5
5
  end
6
6
  end
@@ -5,7 +5,7 @@ gem="$1"
5
5
 
6
6
  update-gem-for-version() {
7
7
  local version="$1"
8
- (export RBENV_VERSION=$version; bundle update "$gem" && bundle exec appraisal update "$gem")
8
+ (export RBENV_VERSION=$version; bundle update "$gem"; bundle exec appraisal update "$gem")
9
9
  }
10
10
 
11
11
  for version in $SUPPORTED_VERSIONS; do
@@ -4,7 +4,7 @@ SUPPORTED_VERSIONS=$(<script/SUPPORTED_VERSIONS)
4
4
 
5
5
  update-gems-for-version() {
6
6
  local version="$1"
7
- (export RBENV_VERSION=$version; bundle update "${@:2}" && bundle exec appraisal update "${@:2}")
7
+ (export RBENV_VERSION=$version; bundle update "${@:2}"; bundle exec appraisal update "${@:2}")
8
8
  }
9
9
 
10
10
  for version in $SUPPORTED_VERSIONS; do
@@ -25,6 +25,7 @@ describe 'shoulda-matchers integrates with multiple libraries' do
25
25
 
26
26
  add_rspec_file 'spec/models/user_spec.rb', <<-FILE
27
27
  describe User do
28
+ subject { User.new(name: "John Smith") }
28
29
  it { should validate_presence_of(:name) }
29
30
  it { should validate_uniqueness_of(:name) }
30
31
  end
@@ -43,9 +44,11 @@ describe 'shoulda-matchers integrates with multiple libraries' do
43
44
  it 'allows the use of matchers from both libraries' do
44
45
  result = run_rspec_suite
45
46
  expect(result).to have_output('2 examples, 0 failures')
46
- expect(result).to have_output('should require name to be set')
47
47
  expect(result).to have_output(
48
- 'should require case sensitive unique value for name'
48
+ 'should validate that :name cannot be empty/falsy'
49
+ )
50
+ expect(result).to have_output(
51
+ 'should validate that :name is case-sensitively unique'
49
52
  )
50
53
  end
51
54
  end
@@ -124,7 +124,9 @@ describe 'shoulda-matchers integrates with Rails' do
124
124
  result = run_n_unit_test_suite
125
125
 
126
126
  expect(result).to indicate_that_tests_were_run(unit: 1, functional: 1)
127
- expect(result).to have_output('User should require name to be set')
127
+ expect(result).to have_output(
128
+ 'User should validate that :name cannot be empty/falsy'
129
+ )
128
130
  expect(result).to have_output('should respond with 200')
129
131
  end
130
132
 
@@ -146,7 +148,9 @@ describe 'shoulda-matchers integrates with Rails' do
146
148
  result = run_rspec_suite
147
149
 
148
150
  expect(result).to have_output('2 examples, 0 failures')
149
- expect(result).to have_output('should require name to be set')
151
+ expect(result).to have_output(
152
+ 'should validate that :name cannot be empty/falsy'
153
+ )
150
154
  expect(result).to have_output('should respond with 200')
151
155
  end
152
156
  end
@@ -13,9 +13,7 @@ RSpec.configure do |config|
13
13
  c.syntax = :expect
14
14
  end
15
15
 
16
- if config.files_to_run.one?
17
- config.default_formatter = 'doc'
18
- end
16
+ config.default_formatter = 'doc'
19
17
 
20
18
  config.mock_with :rspec
21
19
  end
@@ -18,6 +18,7 @@ module AcceptanceTests
18
18
  end
19
19
 
20
20
  def create_generic_bundler_project
21
+ fs.clean
21
22
  fs.create
22
23
  run_command! 'bundle init'
23
24
  end
@@ -61,7 +62,9 @@ module AcceptanceTests
61
62
  end
62
63
 
63
64
  def create_rails_application
64
- command = "bundle exec rails new #{fs.project_directory} --skip-bundle"
65
+ fs.clean
66
+
67
+ command = "bundle exec rails new #{fs.project_directory} --skip-bundle --no-rc"
65
68
 
66
69
  run_command!(command) do |runner|
67
70
  runner.directory = nil
@@ -8,21 +8,33 @@ module Tests
8
8
  include Singleton
9
9
 
10
10
  def assert_appraisal!
11
- unless appraisal?
11
+ unless appraisal_in_use?
12
12
  message = <<EOT
13
13
 
14
14
 
15
15
  Please run tests starting with `appraisal <appraisal_name>`.
16
- Possible appraisals are: #{possible_appraisals}
16
+ Possible appraisals are: #{available_appraisals}
17
17
 
18
18
  EOT
19
19
  raise AppraisalNotSpecified, message
20
20
  end
21
21
  end
22
22
 
23
+ def appraisal_in_use?
24
+ path.dirname == root.join('gemfiles')
25
+ end
26
+
27
+ def current_or_latest_appraisal
28
+ current_appraisal || latest_appraisal
29
+ end
30
+
31
+ def latest_appraisal
32
+ available_appraisals.sort.last
33
+ end
34
+
23
35
  private
24
36
 
25
- def possible_appraisals
37
+ def available_appraisals
26
38
  appraisals = []
27
39
 
28
40
  Appraisal::File.each do |appraisal|
@@ -32,12 +44,14 @@ EOT
32
44
  appraisals
33
45
  end
34
46
 
35
- def path
36
- Bundler.default_gemfile
47
+ def current_appraisal
48
+ if appraisal_in_use?
49
+ File.basename(path, ".gemfile")
50
+ end
37
51
  end
38
52
 
39
- def appraisal?
40
- path.dirname == root.join('gemfiles')
53
+ def path
54
+ Bundler.default_gemfile
41
55
  end
42
56
 
43
57
  def root
@@ -0,0 +1,54 @@
1
+ module UnitTests
2
+ module ActiveRecord
3
+ class CreateTable
4
+ def self.call(table_name, columns)
5
+ new(table_name, columns).call
6
+ end
7
+
8
+ def initialize(table_name, columns)
9
+ @table_name = table_name
10
+ @columns = columns
11
+ end
12
+
13
+ def call
14
+ if columns.key?(:id) && columns[:id] == false
15
+ columns.delete(:id)
16
+ UnitTests::ModelBuilder.create_table(
17
+ table_name,
18
+ id: false,
19
+ &method(:add_columns_to_table)
20
+ )
21
+ else
22
+ UnitTests::ModelBuilder.create_table(
23
+ table_name,
24
+ &method(:add_columns_to_table)
25
+ )
26
+ end
27
+ end
28
+
29
+ protected
30
+
31
+ attr_reader :table_name, :columns
32
+
33
+ private
34
+
35
+ def add_columns_to_table(table)
36
+ columns.each do |column_name, column_specification|
37
+ add_column_to_table(table, column_name, column_specification)
38
+ end
39
+ end
40
+
41
+ def add_column_to_table(table, column_name, column_specification)
42
+ if column_specification.is_a?(Hash)
43
+ column_type = column_specification.fetch(:type)
44
+ column_options = column_specification.fetch(:options, {})
45
+ else
46
+ column_type = column_specification
47
+ column_options = {}
48
+ end
49
+
50
+ table.column(column_name, column_type, column_options)
51
+ end
52
+ end
53
+ end
54
+ end